Merge pull request 'fix-statistics' (#89) from fix-statistics into master
All checks were successful
Java CI with Maven / test (push) Successful in 36s
Java CI with Maven / build-and-push-frontend (push) Successful in 2m15s
Java CI with Maven / build-and-push-backend (push) Successful in 1m22s

Reviewed-on: #89
This commit is contained in:
sebastian 2023-12-20 18:29:52 +01:00
commit 84123f6664
16 changed files with 432 additions and 111 deletions

View File

@ -23,6 +23,7 @@ import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -59,7 +60,14 @@ public class StatisticController {
return ResponseEntity.status(404).body(new SimpleStatusResponse("failed")); return ResponseEntity.status(404).body(new SimpleStatusResponse("failed"));
} }
return ResponseEntity.ok(taskgroupPermissionResult.getResult().calcActivityInfo(includeSubTaskgroups, List<TaskgroupActivityInfo> activityInfos = taskgroupPermissionResult.getResult().calcActivityInfo(includeSubTaskgroups,
LocalDate.parse(startingDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")), LocalDate.parse(endingDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")))); LocalDate.parse(startingDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")), LocalDate.parse(endingDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
activityInfos.sort(new Comparator<TaskgroupActivityInfo>() {
@Override
public int compare(TaskgroupActivityInfo o1, TaskgroupActivityInfo o2) {
return o1.getDate().compareTo(o2.getDate());
}
});
return ResponseEntity.ok(activityInfos);
} }
} }

View File

@ -17,4 +17,12 @@ public class TaskgroupActivityInfo {
this.date = localDate; this.date = localDate;
this.activeMinutes = activeMinutes; this.activeMinutes = activeMinutes;
} }
public LocalDate getDate() {
return date;
}
public int getActiveMinutes() {
return activeMinutes;
}
} }

View File

@ -11,6 +11,9 @@ public class TaskgroupPathInfo {
@JsonProperty @JsonProperty
private String taskgroupPath; private String taskgroupPath;
@JsonProperty
private TaskgroupEntityInfo rootTasktroup;
@JsonProperty @JsonProperty
private List<TaskgroupEntityInfo> directChildren; private List<TaskgroupEntityInfo> directChildren;
public TaskgroupPathInfo(Taskgroup taskgroup) { public TaskgroupPathInfo(Taskgroup taskgroup) {
@ -21,6 +24,7 @@ public class TaskgroupPathInfo {
stringBuilder.append("/"); stringBuilder.append("/");
} }
this.taskgroupPath = stringBuilder.substring(0, stringBuilder.length()-1); this.taskgroupPath = stringBuilder.substring(0, stringBuilder.length()-1);
this.rootTasktroup = new TaskgroupEntityInfo(taskgroup);
directChildren = taskgroup.getChildren().stream().map(TaskgroupEntityInfo::new).toList(); directChildren = taskgroup.getChildren().stream().map(TaskgroupEntityInfo::new).toList();
} }
} }

View File

@ -18,5 +18,6 @@ export interface TaskgroupPathInfo {
*/ */
taskgroupPath: string; taskgroupPath: string;
directChildren: Array<TaskgroupEntityInfo>; directChildren: Array<TaskgroupEntityInfo>;
rootTasktroup: TaskgroupEntityInfo;
} }

View File

@ -83,6 +83,8 @@ import { DraggableSchedulerComponent } from './schedules/draggable-scheduler/dra
import { TaskgroupActivityComponent } from './statistics/taskgroup-activity/taskgroup-activity.component'; import { TaskgroupActivityComponent } from './statistics/taskgroup-activity/taskgroup-activity.component';
import {NgxSliderModule} from "ngx-slider-v2"; import {NgxSliderModule} from "ngx-slider-v2";
import {NgApexchartsModule} from "ng-apexcharts"; import {NgApexchartsModule} from "ng-apexcharts";
import { SimpleActivityDiagramComponent } from './statistics/taskgroup-activity/simple-activity-diagram/simple-activity-diagram.component';
import { HeatmapActivityComponent } from './statistics/taskgroup-activity/heatmap-activity/heatmap-activity.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -124,6 +126,8 @@ import {NgApexchartsModule} from "ng-apexcharts";
DateTimePickerComponent, DateTimePickerComponent,
DraggableSchedulerComponent, DraggableSchedulerComponent,
TaskgroupActivityComponent, TaskgroupActivityComponent,
SimpleActivityDiagramComponent,
HeatmapActivityComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,8 @@
<div style="text-align:center">
<apx-chart
[series]="chartOptions.series!"
[chart]="chartOptions.chart!"
[xaxis]="chartOptions.xaxis!"
[title]="chartOptions.title!"
></apx-chart>
</div>

View File

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeatmapActivityComponent } from './heatmap-activity.component';
describe('HeatmapActivityComponent', () => {
let component: HeatmapActivityComponent;
let fixture: ComponentFixture<HeatmapActivityComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [HeatmapActivityComponent]
});
fixture = TestBed.createComponent(HeatmapActivityComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,168 @@
import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {ApexAxisChartSeries, ChartComponent, ChartType} from "ng-apexcharts";
import {ChartOptions} from "../taskgroup-activity.component";
import * as moment from "moment";
import {HistoryService, TaskgroupActivityInfo, TaskgroupPathInfo} from "../../../../api";
interface XYData {
x: any,
y: any
}
@Component({
selector: 'app-heatmap-activity',
templateUrl: './heatmap-activity.component.html',
styleUrls: ['./heatmap-activity.component.css']
})
export class HeatmapActivityComponent implements OnInit{
@ViewChild("chart") chart?: ChartComponent;
public chartOptions: Partial<ChartOptions> = this.generateChartOptions()
@Input() selectedTaskgroupPath?: TaskgroupPathInfo
constructor(private historyService: HistoryService) {
}
ngOnInit() {
this.chartOptions = this.generateChartOptions();
}
generateChartOptions(): Partial<ChartOptions> {
return {
series: this.generateSeries(),
chart: {
height: 350,
type: 'heatmap',
},
title: {
text: ""
},
dataLabels: {
enabled: false
},
colors: ["#008FFB"],
};
}
generateSeries() : ApexAxisChartSeries {
const series: ApexAxisChartSeries = []
if(this.selectedTaskgroupPath != undefined) {
const startingDate = new Date(2023, 4, 9);
const endDate = new Date(2023, 11, 20);
let activityInfos: TaskgroupActivityInfo[];
this.historyService.statisticsTaskgroupActivityTaskgroupIDStartingDateEndingDateIncludeSubTaskgroupsGet(
this.selectedTaskgroupPath.rootTasktroup.taskgroupID,
moment().subtract(6, "months").format("YYYY-MM-DD"),
moment().add(2, 'days').format("YYYY-MM-DD"),
true
).subscribe({
next: resp => {
activityInfos = resp;
const data: XYData[][] = [];
let currentDate: moment.Moment = moment(activityInfos[0].date).subtract(moment(activityInfos[0].date).day(), 'days');
//Offset until data starts
let index = currentDate.day();
while(currentDate < moment(activityInfos[0].date)) {
if(data[currentDate.day()] == undefined) {
data[currentDate.day()] = [];
}
data[currentDate.day()][index] = {
x: moment(currentDate).toDate(),
y: 0
}
currentDate.add(1, 'days');
}
//inside data
activityInfos.forEach(activityInfo => {
const momentDate: moment.Moment = moment(activityInfo.date);
const seriesIndex = momentDate.day();
if(data[seriesIndex] == undefined) {
data[seriesIndex] = [];
}
data[seriesIndex][index] = {
x: activityInfo.date,
y: activityInfo.activeMinutes
}
if(seriesIndex == 6) {
index++;
}
})
currentDate = moment(activityInfos[activityInfos.length-1].date);
currentDate = currentDate.add(1, "days");
//offset outside data
for(let i=moment(activityInfos[activityInfos.length-1].date).day(); i<7; i++) {
data[i][index] = {
x: moment(currentDate).toDate(),
y: 0
}
console.log(currentDate)
currentDate = currentDate.add(1, "days");
}
series.push({
name: "Saturday",
data: data[6]
});
series.push({
name: "Friday",
data: data[5]
});
series.push({
name: "Thursday",
data: data[4]
});
series.push({
name: "Wednesday",
data: data[3]
});
series.push({
name: "Tuesday",
data: data[2]
});
series.push({
name: "Monday",
data: data[1]
});
series.push({
name: "Sunday",
data: data[0]
});
}
})
return series;
} else {
return series;
}
}
generateData(start: moment.Moment, end: moment.Moment) {
const data: TaskgroupActivityInfo[] = [];
let currentDate: moment.Moment = moment(start);
while(currentDate <= end) {
data.push({
date: currentDate.format("YYYY-MM-DD"),
activeMinutes: Math.floor(Math.random() * (100 + 1))
})
currentDate = currentDate.add(1, 'days');
}
return data;
}
setSelectedTaskgroupPath(taskgroupPath: TaskgroupPathInfo) {
this.selectedTaskgroupPath = taskgroupPath;
this.chartOptions = this.generateChartOptions();
}
}

View File

@ -0,0 +1,12 @@
<div style="text-align:center">
<apx-chart
[series]="chartOptions.series!"
[chart]="chartOptions.chart!"
[xaxis]="chartOptions.xaxis!"
[title]="chartOptions.title!"
></apx-chart>
</div>
<div class="custom-slider">
<ngx-slider class="ngx-slider" [options]="options" [formControl]="sliderControl"
(userChange)="onUserChange($event)"></ngx-slider>
</div>

View File

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleActivityDiagramComponent } from './simple-activity-diagram.component';
describe('SimpleActivityDiagramComponent', () => {
let component: SimpleActivityDiagramComponent;
let fixture: ComponentFixture<SimpleActivityDiagramComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SimpleActivityDiagramComponent]
});
fixture = TestBed.createComponent(SimpleActivityDiagramComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,146 @@
import {Component, Input, ViewChild} from '@angular/core';
import {FormControl} from "@angular/forms";
import {ChangeContext, LabelType, Options} from "ngx-slider-v2";
import {ApexAxisChartSeries, ChartComponent, ChartType} from "ng-apexcharts";
import {HistoryService, TaskgroupPathInfo, TaskgroupService} from "../../../../api";
import * as moment from "moment/moment";
import {ChartOptions} from "../taskgroup-activity.component";
@Component({
selector: 'app-simple-activity-diagram',
templateUrl: './simple-activity-diagram.component.html',
styleUrls: ['./simple-activity-diagram.component.css']
})
export class SimpleActivityDiagramComponent {
@Input('selectedChartype') selectedChartype: string = "bar";
@Input() selectedTaskgroupPath: TaskgroupPathInfo | undefined;
sliderControl: FormControl = new FormControl()
dateRange: Date[] = this.createDateRange();
selectedDateRange: Date[] = this.dateRange;
value: number = this.dateRange[0].getTime();
options: Options = {
stepsArray: this.dateRange.map((date: Date) => {
return { value: date.getTime() };
}),
translate: (value: number, label: LabelType): string => {
return new Date(value).toDateString();
},
floor: 0,
ceil: this.dateRange.length,
};
@ViewChild("chart") chart?: ChartComponent;
public chartOptions: Partial<ChartOptions> = this.generateChartOptions()
constructor(private taskgroupService: TaskgroupService,
private historyService: HistoryService) {
}
createDateRange(): Date[] {
const dates: Date[] = [];
for (let i: number = 1; i <= 31; i++) {
dates.push(moment().subtract(30-i, 'd').toDate());
}
this.sliderControl.setValue([dates[0], dates[dates.length-1]])
return dates;
}
createDateRangeBetween(minValue: moment.Moment, maxValue: moment.Moment) {
const dates: Date[] = [];
let currentDate = moment(minValue);
while(currentDate <= maxValue) {
dates.push(currentDate.toDate());
currentDate = currentDate.add(1, 'days');
}
return dates;
}
onUserChange(changeContext: ChangeContext) {
console.log("min " + moment(changeContext.value).format("YYYY-MM-DD"));
console.log("max " + moment(changeContext.highValue!).format("YYYY-MM-DD"))
this.selectedDateRange = this.createDateRangeBetween(moment(changeContext.value), moment(changeContext.highValue!))
this.chartOptions = this.generateChartOptions()
}
generateChartOptions(): Partial<ChartOptions> {
return {
series: this.generateSeries(),
chart: {
height: 350,
type: this.selectedChartype as ChartType,
stacked: true
},
title: {
text: ""
},
xaxis: {
categories: this.selectedDateRange.map(date => date.toDateString())
}
};
}
generateSeries() : ApexAxisChartSeries {
const series: ApexAxisChartSeries = []
if(this.selectedTaskgroupPath != undefined) {
this.historyService.statisticsTaskgroupActivityTaskgroupIDStartingDateEndingDateIncludeSubTaskgroupsGet(
this.selectedTaskgroupPath!.rootTasktroup.taskgroupID,
moment(this.selectedDateRange[0]).format("YYYY-MM-DD"),
moment(this.selectedDateRange[this.selectedDateRange.length-1]).format("YYYY-MM-DD"),
false
).subscribe({
next: resp => {
series.push(
{
name: this.selectedTaskgroupPath!.rootTasktroup.taskgroupName,
data: resp.map(dailyActivityInfo => dailyActivityInfo.activeMinutes)
}
);
}
})
}
this.selectedTaskgroupPath?.directChildren.forEach(taskgroup => {
this.historyService.statisticsTaskgroupActivityTaskgroupIDStartingDateEndingDateIncludeSubTaskgroupsGet(
taskgroup.taskgroupID,
moment(this.selectedDateRange[0]).format("YYYY-MM-DD"),
moment(this.selectedDateRange[this.selectedDateRange.length-1]).format("YYYY-MM-DD"), true
).subscribe({
next: resp => {
series.push(
{
name: taskgroup.taskgroupName,
data: resp.map(dailyActivityInfo => dailyActivityInfo.activeMinutes)
}
)
}
})
})
console.log(series);
return series;
}
updateSerieSelection() {
this.chartOptions = this.generateChartOptions()
}
setSelectedChartType(selectedChartype: string) {
this.selectedChartype = selectedChartype;
this.updateSerieSelection();
}
setSelectedTaskgroupPath(selectedTaskgroupPath: TaskgroupPathInfo) {
this.selectedTaskgroupPath = selectedTaskgroupPath;
this.updateSerieSelection();
}
}

View File

@ -1,28 +1,18 @@
<div class="container"> <div class="container">
<app-navigation-link-list #navLinkList [navigationLinks]="defaultNavigationLinkPath"></app-navigation-link-list> <app-navigation-link-list #navLinkList [navigationLinks]="defaultNavigationLinkPath"></app-navigation-link-list>
<mat-form-field style="width: 90%"> <mat-form-field style="width: 90%">
<mat-label>Toppings</mat-label> <mat-label>Taskgroup</mat-label>
<mat-select [(ngModel)]="selectedTaskgroupPath" (ngModelChange)="updateSerieSelection()"> <mat-select [(ngModel)]="selectedTaskgroupPath" (ngModelChange)="onSelectTaskgroupPath()">
<mat-option *ngFor="let topping of taskgroupPaths" [value]="topping">{{topping.taskgroupPath}}</mat-option> <mat-option *ngFor="let topping of taskgroupPaths" [value]="topping">{{topping.taskgroupPath}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field style="width: 10%;"> <mat-form-field style="width: 10%;">
<mat-label>Toppings</mat-label> <mat-label>ChartType</mat-label>
<mat-select [(ngModel)]="selectedChartype" (ngModelChange)="updateSerieSelection()"> <mat-select [(ngModel)]="selectedChartype" (ngModelChange)="onSelectChartType()">
<mat-option *ngFor="let topping of availableChartTypes" [value]="topping">{{topping}}</mat-option> <mat-option *ngFor="let topping of availableChartTypes" [value]="topping">{{topping}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div style="text-align:center"> <app-simple-activity-diagram *ngIf="selectedChartype !== 'heatmap'" #simpleActivityDiagram [selectedChartype]="selectedChartype" [selectedTaskgroupPath]="selectedTaskgroupPath"></app-simple-activity-diagram>
<apx-chart <app-heatmap-activity *ngIf="selectedChartype === 'heatmap'" #heatMap></app-heatmap-activity>
[series]="chartOptions.series!"
[chart]="chartOptions.chart!"
[xaxis]="chartOptions.xaxis!"
[title]="chartOptions.title!"
></apx-chart>
</div>
<div class="custom-slider">
<ngx-slider class="ngx-slider" [options]="options" [formControl]="sliderControl" (userChangeStart)="onUserChangeStart($event)"
(userChangeEnd)="onUserChangeStop($event)" (userChange)="onUserChange($event)"></ngx-slider>
</div>
</div> </div>

View File

@ -7,17 +7,22 @@ import {
ApexAxisChartSeries, ApexAxisChartSeries,
ApexChart, ApexChart,
ApexXAxis, ApexXAxis,
ApexTitleSubtitle, ChartType ApexTitleSubtitle, ChartType, ApexPlotOptions, ApexDataLabels
} from "ng-apexcharts"; } from "ng-apexcharts";
import {timeInterval} from "rxjs"; import {timeInterval} from "rxjs";
import {FormControl} from "@angular/forms"; import {FormControl} from "@angular/forms";
import {HistoryService, TaskgroupPathInfo, TaskgroupService} from "../../../api"; import {HistoryService, TaskgroupPathInfo, TaskgroupService} from "../../../api";
import {SimpleActivityDiagramComponent} from "./simple-activity-diagram/simple-activity-diagram.component";
import {HeatmapActivityComponent} from "./heatmap-activity/heatmap-activity.component";
export type ChartOptions = { export type ChartOptions = {
series: ApexAxisChartSeries; series: ApexAxisChartSeries;
chart: ApexChart; chart: ApexChart;
xaxis: ApexXAxis; xaxis: ApexXAxis;
title: ApexTitleSubtitle; title: ApexTitleSubtitle;
plotOptions: ApexPlotOptions,
colors: any,
dataLabels: ApexDataLabels;
}; };
@Component({ @Component({
selector: 'app-taskgroup-activity', selector: 'app-taskgroup-activity',
@ -40,32 +45,13 @@ export class TaskgroupActivityComponent implements OnInit{
} }
]; ];
@ViewChild('simpleActivityDiagram') simpleActivityDiagram ?: SimpleActivityDiagramComponent
@ViewChild('heatMap') heatMap ?: HeatmapActivityComponent
selectedChartype: string = "bar"; selectedChartype: string = "bar";
availableChartTypes: string[] = ["bar", "line", "area"] availableChartTypes: string[] = ["bar", "line", "area", "heatmap"]
selectedTaskgroupPath: TaskgroupPathInfo | undefined selectedTaskgroupPath: TaskgroupPathInfo | undefined
taskgroupPaths: TaskgroupPathInfo[] = [] taskgroupPaths: TaskgroupPathInfo[] = []
sliderControl: FormControl = new FormControl()
dateRange: Date[] = this.createDateRange();
selectedDateRange: Date[] = this.dateRange;
value: number = this.dateRange[0].getTime();
options: Options = {
stepsArray: this.dateRange.map((date: Date) => {
return { value: date.getTime() };
}),
translate: (value: number, label: LabelType): string => {
return new Date(value).toDateString();
},
floor: 0,
ceil: this.dateRange.length,
};
@ViewChild("chart") chart?: ChartComponent;
public chartOptions: Partial<ChartOptions> = this.generateChartOptions()
constructor(private taskgroupService: TaskgroupService,
private historyService: HistoryService) {
}
ngOnInit() { ngOnInit() {
this.taskgroupService.taskgroupsPathGet().subscribe({ this.taskgroupService.taskgroupsPathGet().subscribe({
@ -75,82 +61,22 @@ export class TaskgroupActivityComponent implements OnInit{
}) })
} }
constructor(private taskgroupService: TaskgroupService) {
createDateRange(): Date[] {
const dates: Date[] = [];
for (let i: number = 1; i <= 31; i++) {
dates.push(moment().subtract(30-i, 'd').toDate());
}
this.sliderControl.setValue([dates[0], dates[dates.length-1]])
return dates;
}
createDateRangeBetween(minValue: moment.Moment, maxValue: moment.Moment) {
const dates: Date[] = [];
const numberDays = maxValue.diff(minValue, 'days');
for(let i=0; i<=numberDays; i++) {
dates.push(minValue.add(i, 'd').toDate());
}
return dates;
}
onUserChangeStart(changeContext: ChangeContext) {
//console.log("onUserChangeStart" + new Date(changeContext.value));
}
onUserChangeStop(changeContext: ChangeContext) {
//console.log("onUserChangeStop" + new Date(changeContext.highValue!))
}
onUserChange(changeContext: ChangeContext) {
const dateRange = this.createDateRangeBetween(moment(changeContext.value), moment(changeContext.highValue!))
this.chartOptions = this.generateChartOptions()
}
generateChartOptions(): Partial<ChartOptions> {
return {
series: this.generateSeries(),
chart: {
height: 350,
type: this.selectedChartype as ChartType,
stacked: true
},
title: {
text: ""
},
xaxis: {
categories: this.selectedDateRange.map(date => date.toDateString())
}
};
}
generateSeries() : ApexAxisChartSeries {
const series: ApexAxisChartSeries = []
this.selectedTaskgroupPath?.directChildren.forEach(taskgroup => {
this.historyService.statisticsTaskgroupActivityTaskgroupIDStartingDateEndingDateIncludeSubTaskgroupsGet(
taskgroup.taskgroupID,
moment(this.selectedDateRange[0]).format("YYYY-MM-DD"),
moment(this.selectedDateRange[this.selectedDateRange.length-1]).format("YYYY-MM-DD"), true
).subscribe({
next: resp => {
series.push(
{
name: taskgroup.taskgroupName,
data: resp.map(dailyActivityInfo => dailyActivityInfo.activeMinutes)
}
)
}
})
})
return series;
} }
updateSerieSelection() { onSelectTaskgroupPath() {
this.chartOptions = this.generateChartOptions() if(this.simpleActivityDiagram != undefined) {
this.simpleActivityDiagram.setSelectedTaskgroupPath(this.selectedTaskgroupPath!);
}
this.heatMap?.setSelectedTaskgroupPath(this.selectedTaskgroupPath!);
}
onSelectChartType() {
if(this.simpleActivityDiagram != undefined) {
this.simpleActivityDiagram.setSelectedChartType(this.selectedChartype);
} }
} }
}

View File

@ -2612,6 +2612,7 @@ components:
required: required:
- taskgroupPath - taskgroupPath
- directChildren - directChildren
- rootTasktroup
additionalProperties: false additionalProperties: false
properties: properties:
taskgroupPath: taskgroupPath:
@ -2621,6 +2622,9 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/TaskgroupEntityInfo' $ref: '#/components/schemas/TaskgroupEntityInfo'
rootTasktroup:
type: object
$ref: '#/components/schemas/TaskgroupEntityInfo'
TaskgroupActivityInfo: TaskgroupActivityInfo:
required: required:
- date - date