diff --git a/common/taskana-common-data/src/main/resources/sql/sample-data/task.sql b/common/taskana-common-data/src/main/resources/sql/sample-data/task.sql index 0479fe55c..6815b4f7c 100644 --- a/common/taskana-common-data/src/main/resources/sql/sample-data/task.sql +++ b/common/taskana-common-data/src/main/resources/sql/sample-data/task.sql @@ -91,6 +91,6 @@ INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000074', 'ETI:0000000 INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000075', 'ETI:000000000000000000000000000000000075', RELATIVE_DATE(-4) , RELATIVE_DATE(-2) , RELATIVE_DATE(0) , RELATIVE_DATE(0) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'TaskXY' , 'creator_user_id' , 'TaskXY' , null , 3 , -1 , 'COMPLETED' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000008' , 'USER-2-1' , 'DOMAIN_A', 'PI_0000000000075' , 'DOC_0000000000000000075' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000076', 'ETI:000000000000000000000000000000000076', RELATIVE_DATE(0) , null , null , RELATIVE_DATE(0) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'Schadenfall' , 'creator_user_id' , 'Schadenfall' , null , 1 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000009' , 'USER-2-2' , 'DOMAIN_A', 'PI_0000000000076' , 'DOC_0000000000000000076' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , false , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000077', 'ETI:000000000000000000000000000000000077', RELATIVE_DATE(0) , null , null , RELATIVE_DATE(0) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'Widerruf' , 'creator_user_id' , 'Wideruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'T2000' , 'CLI:100000000000000000000000000000000016', 'WBI:100000000000000000000000000000000009' , 'USER-2-2' , 'DOMAIN_A', 'PI_0000000000077' , 'DOC_0000000000000000077' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , false , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); -INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000078', 'ETI:000000000000000000000000000000000078', RELATIVE_DATE(-2) , RELATIVE_DATE(-1) , null , RELATIVE_DATE(-1) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'TaskXY' , 'creator_user_id' , 'TaskXY' , null , 2 , -1 , 'CLAIMED' , 'EXTERN' , 'T6310' , 'CLI:000000000000000000000000000000000011', 'WBI:100000000000000000000000000000000009' , 'USER-2-2' , 'DOMAIN_A', 'PI_0000000000078' , 'DOC_0000000000000000078' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); -INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000079', 'ETI:000000000000000000000000000000000079', RELATIVE_DATE(-1) , null , null , RELATIVE_DATE(-1) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'Schadenfall' , 'creator_user_id' , 'Schadenfall' , null , 3 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000009' , 'USER-2-2' , 'DOMAIN_A', 'PI_0000000000079' , 'DOC_0000000000000000079' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); -INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000080', 'ETI:000000000000000000000000000000000080', RELATIVE_DATE(-4) , RELATIVE_DATE(-2) , RELATIVE_DATE(0) , RELATIVE_DATE(0) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'TaskXY' , 'creator_user_id' , 'TaskXY' , null , 3 , -1 , 'COMPLETED' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000009' , 'USER-2-2' , 'DOMAIN_A', 'PI_0000000000080' , 'DOC_0000000000000000080' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); +INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000078', 'ETI:000000000000000000000000000000000078', RELATIVE_DATE(-2) , RELATIVE_DATE(-1) , null , RELATIVE_DATE(-1) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'TaskXY' , 'creator_user_id' , 'TaskXY' , null , 2 , -1 , 'CLAIMED' , 'EXTERN' , 'T6310' , 'CLI:000000000000000000000000000000000011', 'WBI:100000000000000000000000000000000016' , 'TPK_VIP_2' , 'DOMAIN_A', 'PI_0000000000078' , 'DOC_0000000000000000078' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); +INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000079', 'ETI:000000000000000000000000000000000079', RELATIVE_DATE(-1) , null , null , RELATIVE_DATE(-1) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'Schadenfall' , 'creator_user_id' , 'Schadenfall' , null , 3 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000010' , 'TPK_VIP' , 'DOMAIN_A', 'PI_0000000000079' , 'DOC_0000000000000000079' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); +INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000080', 'ETI:000000000000000000000000000000000080', RELATIVE_DATE(-4) , RELATIVE_DATE(-2) , RELATIVE_DATE(0) , RELATIVE_DATE(0) , null , RELATIVE_DATE(0) , RELATIVE_DATE(1) , 'TaskXY' , 'creator_user_id' , 'TaskXY' , null , 260 , -1 , 'COMPLETED' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000010' , 'TPK_VIP' , 'DOMAIN_A', 'PI_0000000000080' , 'DOC_0000000000000000080' , null , '00' , 'PASystem' , '00' , 'SDNR' , '98769432' , true , false , null , 'NONE' , null , null , null , null , null , null , null , null , null ,null , null , null , null , null , 'abc' , null , null , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java index e500a1569..33d2af219 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/monitor/rest/MonitorControllerIntTest.java @@ -125,6 +125,6 @@ class MonitorControllerIntTest { assertThat(report.getSumRow()) .extracting(RowRepresentationModel::getCells) - .containsExactly(new int[] {23, 0, 0}); + .containsExactly(new int[] {25, 1, 0}); } } diff --git a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java index 91b07d932..a8bd3ae51 100644 --- a/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java +++ b/rest/taskana-rest-spring/src/test/java/pro/taskana/task/rest/TaskControllerIntTest.java @@ -91,7 +91,7 @@ class TaskControllerIntTest { assertThat(response.getBody()).isNotNull(); assertThat((response.getBody()).getLink(IanaLinkRelations.SELF)).isNotNull(); - assertThat(response.getBody().getContent()).hasSize(58); + assertThat(response.getBody().getContent()).hasSize(57); } @Test @@ -497,7 +497,7 @@ class TaskControllerIntTest { TEMPLATE.exchange(url, HttpMethod.GET, auth, TASK_SUMMARY_PAGE_MODEL_TYPE); assertThat(response.getBody()).isNotNull(); - assertThat((response.getBody()).getContent()).hasSize(58); + assertThat((response.getBody()).getContent()).hasSize(57); String url2 = restHelper.toUrl(RestEndpoints.URL_TASKS) diff --git a/web/src/app/monitor/components/canvas/canvas.component.html b/web/src/app/monitor/components/canvas/canvas.component.html new file mode 100644 index 000000000..8ea33058c --- /dev/null +++ b/web/src/app/monitor/components/canvas/canvas.component.html @@ -0,0 +1 @@ + diff --git a/web/src/app/monitor/components/canvas/canvas.component.scss b/web/src/app/monitor/components/canvas/canvas.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/src/app/monitor/components/canvas/canvas.component.spec.ts b/web/src/app/monitor/components/canvas/canvas.component.spec.ts new file mode 100644 index 000000000..e6ef9933c --- /dev/null +++ b/web/src/app/monitor/components/canvas/canvas.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { DebugElement } from '@angular/core'; +import { NgxsModule } from '@ngxs/store'; +import { CanvasComponent } from './canvas.component'; +import { workbasketReportMock } from '../task-priority-report/monitor-mock-data'; + +describe('CanvasComponent', () => { + let fixture: ComponentFixture; + let debugElement: DebugElement; + let component: CanvasComponent; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NgxsModule.forRoot([])], + declarations: [CanvasComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(CanvasComponent); + debugElement = fixture.debugElement; + component = fixture.debugElement.componentInstance; + component.id = '1'; + fixture.detectChanges(); + }) + ); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should show canvas with id from input', () => { + const id = debugElement.nativeElement.querySelector("[id='1']"); + expect(id).toBeTruthy(); + }); + + it('should call generateChart()', () => { + component.generateChart = jest.fn(); + const reportRow = workbasketReportMock.rows[1]; + component.row = reportRow; + fixture.detectChanges(); + component.ngAfterViewInit(); + expect(component.generateChart).toHaveBeenCalledWith('1', reportRow); + }); +}); diff --git a/web/src/app/monitor/components/canvas/canvas.component.ts b/web/src/app/monitor/components/canvas/canvas.component.ts new file mode 100644 index 000000000..7a26703e5 --- /dev/null +++ b/web/src/app/monitor/components/canvas/canvas.component.ts @@ -0,0 +1,52 @@ +import { AfterViewInit, Component, Input } from '@angular/core'; +import { Chart } from 'chart.js'; +import { priorityTypes } from '../../models/priority'; + +import { ReportRow } from '../../models/report-row'; + +@Component({ + selector: 'taskana-monitor-canvas', + templateUrl: './canvas.component.html', + styleUrls: ['./canvas.component.scss'] +}) +export class CanvasComponent implements AfterViewInit { + @Input() row: ReportRow; + @Input() id: string; + @Input() isReversed: boolean; + + ngAfterViewInit() { + const canvas = document.getElementById(this.id) as HTMLCanvasElement; + if (canvas && this.id && this.row) { + this.generateChart(this.id, this.row); + } + } + + generateChart(id: string, row: ReportRow) { + const canvas = document.getElementById(id) as HTMLCanvasElement; + new Chart(canvas, { + type: 'doughnut', + data: { + labels: [priorityTypes.HIGH, priorityTypes.MEDIUM, priorityTypes.LOW], + datasets: [ + { + label: 'Tasks by Priority', + // depends on whether backend sends data sorted in ascending or descending order + data: this.isReversed ? row.cells.reverse() : row.cells, + backgroundColor: ['red', 'gold', 'limegreen'], + borderWidth: 0 + } + ] + }, + options: { + rotation: Math.PI, + circumference: Math.PI, + title: { + display: true, + text: String(row.total), + position: 'bottom', + fontSize: 18 + } + } + }); + } +} diff --git a/web/src/app/monitor/components/monitor/monitor.component.html b/web/src/app/monitor/components/monitor/monitor.component.html index 3ccd220fe..7a8cfd956 100644 --- a/web/src/app/monitor/components/monitor/monitor.component.html +++ b/web/src/app/monitor/components/monitor/monitor.component.html @@ -1,6 +1,8 @@ - \ No newline at end of file + diff --git a/web/src/app/monitor/components/monitor/monitor.component.ts b/web/src/app/monitor/components/monitor/monitor.component.ts index db096ddb4..770f00e76 100644 --- a/web/src/app/monitor/components/monitor/monitor.component.ts +++ b/web/src/app/monitor/components/monitor/monitor.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ @@ -10,10 +10,10 @@ export class MonitorComponent implements OnInit { selectedTab = ''; constructor(public router: Router) { - this.router.navigate(['/taskana/monitor/tasks']); + this.router.navigate(['/taskana/monitor/tasks-priority']); } ngOnInit(): void { - this.selectedTab = 'tasks'; + this.selectedTab = 'tasks-priority'; } } diff --git a/web/src/app/monitor/components/report-table/report-table.component.ts b/web/src/app/monitor/components/report-table/report-table.component.ts index ee15d059d..7fb473021 100644 --- a/web/src/app/monitor/components/report-table/report-table.component.ts +++ b/web/src/app/monitor/components/report-table/report-table.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnInit } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; import { ReportData } from 'app/monitor/models/report-data'; import { ReportRow } from '../../models/report-row'; diff --git a/web/src/app/monitor/components/task-priority-report/monitor-mock-data.ts b/web/src/app/monitor/components/task-priority-report/monitor-mock-data.ts new file mode 100644 index 000000000..83f28e540 --- /dev/null +++ b/web/src/app/monitor/components/task-priority-report/monitor-mock-data.ts @@ -0,0 +1,48 @@ +import { ReportData } from '../../models/report-data'; + +export const workbasketReportMock: ReportData = { + meta: { + name: 'WorkbasketPriorityReport', + date: '2021-08-24T11:44:34.023901Z', + header: ['<249', '250 - 500', '>501'], + rowDesc: ['WORKBASKET'], + sumRowDesc: 'Total' + }, + rows: [ + { + cells: [5, 0, 0], + total: 5, + depth: 0, + desc: ['ADMIN'], + display: true + }, + { + cells: [3, 5, 2], + total: 10, + depth: 0, + desc: ['GPK_KSC'], + display: true + } + ], + sumRow: [ + { + cells: [8, 5, 2], + total: 15, + depth: 0, + desc: ['Total'], + display: true + } + ] +}; + +export const workbasketReportUnexpectedHeaderMock: ReportData = { + meta: { + name: 'WorkbasketPriorityReport', + date: '2021-08-24T11:44:34.023901Z', + header: ['<10', '11 - 100', '>101'], + rowDesc: ['WORKBASKET'], + sumRowDesc: 'Total' + }, + rows: [], + sumRow: [] +}; diff --git a/web/src/app/monitor/components/task-priority-report/task-priority-report.component.html b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.html new file mode 100644 index 000000000..12b2ec583 --- /dev/null +++ b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.html @@ -0,0 +1,48 @@ +
+

{{reportData?.meta.name}} ({{reportData?.meta.date | germanTimeFormat }})

+ +
There are no Workbaskets with type TOPIC.
+ +
+
+ + +
+
{{row.desc[0]}}
+
+ + + + + +
+ + + + + + + + + + + + + + + + + +
Priority + {{element.priority}} Number of Tasks + {{element.number}}
+
+
+
+
diff --git a/web/src/app/monitor/components/task-priority-report/task-priority-report.component.scss b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.scss new file mode 100644 index 000000000..f88bf241c --- /dev/null +++ b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.scss @@ -0,0 +1,40 @@ +table { + width: 100%; +} + +.task-priority-report { + padding: 8px; + overflow-y: scroll; + height: calc(100vh - 104px); + + &__workbaskets { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + + &__workbasket { + display: flex; + flex-direction: column; + width: 360px; + margin: 24px 24px 36px 24px; + } + + &__headline { + text-align: center; + margin-bottom: 8px; + } + + &__row--low { + color: limegreen; + } + + &__row--medium { + color: gold; + } + + &__row--high { + color: red; + } + +} diff --git a/web/src/app/monitor/components/task-priority-report/task-priority-report.component.spec.ts b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.spec.ts new file mode 100644 index 000000000..33716425a --- /dev/null +++ b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.spec.ts @@ -0,0 +1,133 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Component, DebugElement, Input, Pipe, PipeTransform } from '@angular/core'; +import { NgxsModule } from '@ngxs/store'; +import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service'; +import { NotificationService } from '../../../shared/services/notifications/notification.service'; +import { TaskPriorityReportComponent } from './task-priority-report.component'; +import { MonitorService } from '../../services/monitor.service'; +import { of } from 'rxjs'; +import { MatTableModule } from '@angular/material/table'; +import { priorityTypes } from '../../models/priority'; +import { workbasketReportMock, workbasketReportUnexpectedHeaderMock } from './monitor-mock-data'; + +@Pipe({ name: 'germanTimeFormat' }) +class GermanTimeFormatPipe implements PipeTransform { + transform(value: number): number { + return value; + } +} + +@Component({ selector: 'taskana-monitor-canvas', template: '' }) +class CanvasStub { + @Input() row; + @Input() id; + @Input() isReversed; +} + +const monitorServiceSpy: Partial = { + getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportMock)) +}; + +const monitorServiceWithDifferentDataSpy: Partial = { + getTasksByPriorityReport: jest.fn().mockReturnValue(of(workbasketReportUnexpectedHeaderMock)) +}; + +const notificationServiceSpy: Partial = { + showWarning: jest.fn() +}; + +describe('TaskPriorityReportComponent', () => { + let fixture: ComponentFixture; + let debugElement: DebugElement; + let component: TaskPriorityReportComponent; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NgxsModule.forRoot([]), MatTableModule], + declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub], + providers: [ + { provide: MonitorService, useValue: monitorServiceSpy }, + { provide: NotificationService, useValue: notificationServiceSpy } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TaskPriorityReportComponent); + debugElement = fixture.debugElement; + component = fixture.debugElement.componentInstance; + fixture.detectChanges(); + }) + ); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should show Canvas component for all Workbaskets', () => { + const canvas = debugElement.nativeElement.querySelectorAll('taskana-monitor-canvas'); + expect(canvas).toHaveLength(2); + }); + + it('should show table for all Workbaskets', () => { + const table = debugElement.nativeElement.querySelectorAll('table'); + expect(table).toHaveLength(2); + }); + + it('should not show warning when actual header matches the expected header', () => { + const showWarningSpy = jest.spyOn(notificationServiceSpy, 'showWarning'); + component.ngOnInit(); + expect(showWarningSpy).toHaveBeenCalledTimes(0); + }); + + it('should set isReserved to true when high priority has a higher index than low priority', () => { + expect(component.isReversed).toBeTruthy(); + }); + + it('should set tableDataArray', () => { + const expectedTableData = [ + [ + { priority: priorityTypes.HIGH, number: 0 }, + { priority: priorityTypes.MEDIUM, number: 0 }, + { priority: priorityTypes.LOW, number: 5 }, + { priority: 'Total', number: 5 } + ], + [ + { priority: priorityTypes.HIGH, number: 2 }, + { priority: priorityTypes.MEDIUM, number: 5 }, + { priority: priorityTypes.LOW, number: 3 }, + { priority: 'Total', number: 10 } + ] + ]; + expect(component.tableDataArray).toStrictEqual(expectedTableData); + }); +}); + +describe('TaskPriorityReportComponent with report data containing an unexpected header', () => { + let fixture: ComponentFixture; + let debugElement: DebugElement; + let component: TaskPriorityReportComponent; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NgxsModule.forRoot([]), MatTableModule], + declarations: [TaskPriorityReportComponent, GermanTimeFormatPipe, CanvasStub], + providers: [ + WorkbasketService, + { provide: MonitorService, useValue: monitorServiceWithDifferentDataSpy }, + { provide: NotificationService, useValue: notificationServiceSpy } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TaskPriorityReportComponent); + debugElement = fixture.debugElement; + component = fixture.debugElement.componentInstance; + fixture.detectChanges(); + }) + ); + + it('should show warning when actual header does not match the expected header', () => { + const showWarningSpy = jest.spyOn(notificationServiceSpy, 'showWarning'); + expect(showWarningSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web/src/app/monitor/components/task-priority-report/task-priority-report.component.ts b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.ts new file mode 100644 index 000000000..0dc3d417c --- /dev/null +++ b/web/src/app/monitor/components/task-priority-report/task-priority-report.component.ts @@ -0,0 +1,52 @@ +import { Component, OnInit } from '@angular/core'; +import { priorityTypes } from '../../models/priority'; +import { ReportData } from '../../models/report-data'; +import { MonitorService } from '../../services/monitor.service'; +import { take } from 'rxjs/operators'; +import { NotificationService } from '../../../shared/services/notifications/notification.service'; +import { WorkbasketType } from '../../../shared/models/workbasket-type'; + +@Component({ + selector: 'taskana-monitor-task-priority-report', + templateUrl: './task-priority-report.component.html', + styleUrls: ['./task-priority-report.component.scss'] +}) +export class TaskPriorityReportComponent implements OnInit { + columns: string[] = ['priority', 'number']; + reportData: ReportData; + tableDataArray: { priority: string; number: number }[][] = []; + isReversed = true; + + constructor(private monitorService: MonitorService, private notificationService: NotificationService) {} + + ngOnInit() { + this.monitorService + .getTasksByPriorityReport([WorkbasketType.TOPIC]) + .pipe(take(1)) + .subscribe((reportData) => { + this.reportData = reportData; + let indexHigh = reportData.meta.header.indexOf('>501'); + let indexMedium = reportData.meta.header.indexOf('250 - 500'); + let indexLow = reportData.meta.header.indexOf('<249'); + if (indexHigh == -1 || indexMedium == -1 || indexLow == -1) { + this.notificationService.showWarning('REPORT_DATA_WRONG_HEADER'); + indexHigh = 2; + indexMedium = 1; + indexLow = 0; + } + this.isReversed = indexHigh > indexLow; + reportData.rows.forEach((row) => { + this.tableDataArray.push([ + { priority: priorityTypes.HIGH, number: row.cells[indexHigh] }, + { priority: priorityTypes.MEDIUM, number: row.cells[indexMedium] }, + { priority: priorityTypes.LOW, number: row.cells[indexLow] }, + { priority: 'Total', number: row.total } + ]); + }); + }); + } + + toString(i: number): string { + return String(i); + } +} diff --git a/web/src/app/monitor/components/workbasket-report/workbasket-report.component.ts b/web/src/app/monitor/components/workbasket-report/workbasket-report.component.ts index 2f6cc1dde..b2b99b7aa 100644 --- a/web/src/app/monitor/components/workbasket-report/workbasket-report.component.ts +++ b/web/src/app/monitor/components/workbasket-report/workbasket-report.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from '@angular/core'; import { MetaInfoData } from '../../models/meta-info-data'; -import { QueryType } from '../../models/query-type'; enum WorkbasketReports { DUE_DATE, PLANNED_DATE @@ -26,7 +25,7 @@ export class WorkbasketReportComponent implements OnInit { } getTitle(): string { return this.selectedComponent - ? 'Tasks grouped by workbasket, querying by planned date' - : 'Tasks grouped by workbasket, querying by due date'; + ? 'Tasks grouped by Workbasket, querying by planned date' + : 'Tasks grouped by Workbasket, querying by due date'; } } diff --git a/web/src/app/monitor/models/chart-data.ts b/web/src/app/monitor/models/chart-data.ts index d63d8c6ac..e0c1b0911 100644 --- a/web/src/app/monitor/models/chart-data.ts +++ b/web/src/app/monitor/models/chart-data.ts @@ -1,4 +1,4 @@ export class ChartData { - data: Array; + data: number[]; label: string; } diff --git a/web/src/app/monitor/models/meta-info-data.ts b/web/src/app/monitor/models/meta-info-data.ts index 671c418bb..2571f29cf 100644 --- a/web/src/app/monitor/models/meta-info-data.ts +++ b/web/src/app/monitor/models/meta-info-data.ts @@ -1,7 +1,7 @@ export class MetaInfoData { name: string; date: string; - header: Array; - rowDesc: Array; + header: string[]; + rowDesc: string[]; sumRowDesc: string; } diff --git a/web/src/app/monitor/models/priority.ts b/web/src/app/monitor/models/priority.ts new file mode 100644 index 000000000..bc8f9295c --- /dev/null +++ b/web/src/app/monitor/models/priority.ts @@ -0,0 +1,5 @@ +export enum priorityTypes { + HIGH = 'High', + MEDIUM = 'Medium', + LOW = 'Low' +} diff --git a/web/src/app/monitor/models/report-data.ts b/web/src/app/monitor/models/report-data.ts index 559d59418..4185abd3c 100644 --- a/web/src/app/monitor/models/report-data.ts +++ b/web/src/app/monitor/models/report-data.ts @@ -3,6 +3,6 @@ import { MetaInfoData } from './meta-info-data'; export class ReportData { meta: MetaInfoData; - rows: Array; - sumRow: Array; + rows: ReportRow[]; + sumRow: ReportRow[]; } diff --git a/web/src/app/monitor/models/report-row.ts b/web/src/app/monitor/models/report-row.ts index 0200535fc..8ff0e75a3 100644 --- a/web/src/app/monitor/models/report-row.ts +++ b/web/src/app/monitor/models/report-row.ts @@ -1,7 +1,7 @@ -export class ReportRow { - cells: Array; +export interface ReportRow { + cells: number[]; total: number; depth: number; - desc: Array; - display = false; + desc: string[]; + display: boolean; } diff --git a/web/src/app/monitor/monitor-routing.module.ts b/web/src/app/monitor/monitor-routing.module.ts index 2bb4088d9..d6cf6735e 100644 --- a/web/src/app/monitor/monitor-routing.module.ts +++ b/web/src/app/monitor/monitor-routing.module.ts @@ -5,6 +5,7 @@ import { TaskReportComponent } from './components/task-report/task-report.compon import { WorkbasketReportComponent } from './components/workbasket-report/workbasket-report.component'; import { ClassificationReportComponent } from './components/classification-report/classification-report.component'; import { TimestampReportComponent } from './components/timestamp-report/timestamp-report.component'; +import { TaskPriorityReportComponent } from './components/task-priority-report/task-priority-report.component'; const routes: Routes = [ { @@ -12,7 +13,11 @@ const routes: Routes = [ component: MonitorComponent, children: [ { - path: 'tasks', + path: 'tasks-priority', + component: TaskPriorityReportComponent + }, + { + path: 'tasks-status', component: TaskReportComponent }, { diff --git a/web/src/app/monitor/monitor.module.ts b/web/src/app/monitor/monitor.module.ts index 33dfeb80d..c6c6e8c7b 100644 --- a/web/src/app/monitor/monitor.module.ts +++ b/web/src/app/monitor/monitor.module.ts @@ -2,8 +2,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatButtonModule } from '@angular/material/button'; import { AlertModule } from 'ngx-bootstrap/alert'; import { TabsModule } from 'ngx-bootstrap/tabs'; import { ChartsModule } from 'ng2-charts'; @@ -12,6 +10,9 @@ import { MapToIterable } from 'app/shared/pipes/map-to-iterable.pipe'; import { SharedModule } from '../shared/shared.module'; import { MonitorRoutingModule } from './monitor-routing.module'; +/** + * Components + */ import { ReportTableComponent } from './components/report-table/report-table.component'; import { MonitorComponent } from './components/monitor/monitor.component'; import { TaskReportComponent } from './components/task-report/task-report.component'; @@ -20,8 +21,21 @@ import { TimestampReportComponent } from './components/timestamp-report/timestam import { WorkbasketReportComponent } from './components/workbasket-report/workbasket-report.component'; import { WorkbasketReportPlannedDateComponent } from './components/workbasket-report-planned-date/workbasket-report-planned-date.component'; import { WorkbasketReportDueDateComponent } from './components/workbasket-report-due-date/workbasket-report-due-date.component'; +import { TaskPriorityReportComponent } from './components/task-priority-report/task-priority-report.component'; +import { CanvasComponent } from './components/canvas/canvas.component'; + +/** + * Services + */ import { MonitorService } from './services/monitor.service'; +/** + * Material Design + */ +import { MatTabsModule } from '@angular/material/tabs'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTableModule } from '@angular/material/table'; + const MODULES = [ CommonModule, MonitorRoutingModule, @@ -32,11 +46,15 @@ const MODULES = [ HttpClientModule, AngularSvgIconModule, SharedModule, - MatTabsModule + MatTabsModule, + MatButtonModule, + MatTableModule ]; const DECLARATIONS = [ ReportTableComponent, MonitorComponent, + TaskPriorityReportComponent, + CanvasComponent, TimestampReportComponent, WorkbasketReportComponent, WorkbasketReportPlannedDateComponent, @@ -47,7 +65,7 @@ const DECLARATIONS = [ @NgModule({ declarations: DECLARATIONS, - imports: [MODULES, MatButtonModule], + imports: [MODULES], providers: [MonitorService, MapToIterable] }) export class MonitorModule {} diff --git a/web/src/app/monitor/services/monitor.service.ts b/web/src/app/monitor/services/monitor.service.ts index a69146bd0..4a75a8406 100644 --- a/web/src/app/monitor/services/monitor.service.ts +++ b/web/src/app/monitor/services/monitor.service.ts @@ -6,6 +6,7 @@ import { ChartData } from 'app/monitor/models/chart-data'; import { ReportData } from '../models/report-data'; import { asUrlQueryString } from '../../shared/util/query-parameters-v2'; import { TaskState } from '../../shared/models/task-state'; +import { WorkbasketType } from '../../shared/models/workbasket-type'; const monitorUrl = '/v1/monitor/'; @@ -37,7 +38,7 @@ export class MonitorService { states: [TaskState.READY, TaskState.CLAIMED, TaskState.COMPLETED] }; return this.httpClient.get( - `${environment.taskanaRestUrl}/v1/monitor/workbasket-report${asUrlQueryString(queryParams)}` + `${environment.taskanaRestUrl + monitorUrl}workbasket-report${asUrlQueryString(queryParams)}` ); } @@ -49,7 +50,7 @@ export class MonitorService { return this.httpClient.get(`${environment.taskanaRestUrl + monitorUrl}timestamp-report`); } - getChartData(source: ReportData): Array { + getChartData(source: ReportData): ChartData[] { return source.rows.map((row) => { const rowData = new ChartData(); [rowData.label] = row.desc; @@ -57,4 +58,13 @@ export class MonitorService { return rowData; }); } + + getTasksByPriorityReport(type: WorkbasketType[] = []): Observable { + const queryParams = { + 'workbasket-type': type + }; + return this.httpClient.get( + `${environment.taskanaRestUrl + monitorUrl}workbasket-priority-report${asUrlQueryString(queryParams)}` + ); + } } diff --git a/web/src/app/shared/services/notifications/notification.service.ts b/web/src/app/shared/services/notifications/notification.service.ts index 837ad90d2..6d147be6a 100644 --- a/web/src/app/shared/services/notifications/notification.service.ts +++ b/web/src/app/shared/services/notifications/notification.service.ts @@ -55,6 +55,10 @@ export class NotificationService { ); } + showWarning(warningKey: string, messageVariables: object = {}) { + this.toastService.warning(this.obtainMessageService.getMessage(warningKey, messageVariables, messageTypes.WARNING)); + } + showDialog(key: string, messageVariables: object = {}, callback: Function) { const message = this.obtainMessageService.getMessage(key, messageVariables, messageTypes.DIALOG); diff --git a/web/src/app/shared/services/obtain-message/message-by-error-code.ts b/web/src/app/shared/services/obtain-message/message-by-error-code.ts index 316257ac9..7c11ca711 100644 --- a/web/src/app/shared/services/obtain-message/message-by-error-code.ts +++ b/web/src/app/shared/services/obtain-message/message-by-error-code.ts @@ -96,6 +96,12 @@ export const messageByErrorCode = { EMPTY_WORKBASKET: 'Selected Workbasket is empty' }, + [messageTypes.WARNING]: { + REPORT_DATA_WRONG_HEADER: + 'The received header of the Report data does not match the expected header. ' + + 'The data might be displayed incorrectly. Please contact your administrator.' + }, + [messageTypes.DIALOG]: { POPUP_CONFIGURATION: 'This Popup was not configured properly for this request. Please contact your administrator.', diff --git a/web/src/app/shared/services/obtain-message/message-types.ts b/web/src/app/shared/services/obtain-message/message-types.ts index 562c25420..a68d6b714 100644 --- a/web/src/app/shared/services/obtain-message/message-types.ts +++ b/web/src/app/shared/services/obtain-message/message-types.ts @@ -2,5 +2,6 @@ export enum messageTypes { ERROR, SUCCESS, INFORMATION, + WARNING, DIALOG }