Build data of series by data provided through historyService
All checks were successful
Java CI with Maven / test (push) Successful in 34s
Java CI with Maven / build-and-push-frontend (push) Successful in 7s
Java CI with Maven / build-and-push-backend (push) Successful in 7s

This commit is contained in:
Sebastian Böckelmann 2023-11-19 14:52:33 +01:00
parent cf0bbabb85
commit 882169840d
11 changed files with 240 additions and 47 deletions

View File

@ -1,21 +1,30 @@
package core.api.controller; package core.api.controller;
import core.api.models.auth.SimpleStatusResponse;
import core.api.models.timemanager.history.TaskgroupActivityInfo;
import core.api.models.timemanager.history.WorkingStatus; import core.api.models.timemanager.history.WorkingStatus;
import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo;
import core.entities.timemanager.AbstractSchedule; import core.entities.timemanager.AbstractSchedule;
import core.entities.timemanager.Task;
import core.entities.timemanager.Taskgroup;
import core.services.PermissionResult;
import core.services.ServiceExitCode;
import core.services.TaskScheduleService; import core.services.TaskScheduleService;
import core.services.TaskgroupService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.security.core.parameters.P;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
@CrossOrigin(origins = "*", maxAge = 3600) @CrossOrigin(origins = "*", maxAge = 3600)
@RestController @RestController
@ -23,6 +32,7 @@ import java.util.List;
public class StatisticController { public class StatisticController {
@Autowired private TaskScheduleService taskScheduleService; @Autowired private TaskScheduleService taskScheduleService;
@Autowired private TaskgroupService taskgroupService;
@GetMapping("/history/workingStatus") @GetMapping("/history/workingStatus")
public ResponseEntity<?> getWorkingStatus() { public ResponseEntity<?> getWorkingStatus() {
@ -39,4 +49,17 @@ public class StatisticController {
return ResponseEntity.ok(new WorkingStatus(missedSchedules, activeTime)); return ResponseEntity.ok(new WorkingStatus(missedSchedules, activeTime));
} }
@GetMapping("/statistics/taskgroup-activity/{taskgroupID}/{startingDate}/{endingDate}/{includeSubTaskgroups}")
public ResponseEntity<?> getTaskgroupActivity(@PathVariable long taskgroupID, @PathVariable String startingDate, @PathVariable String endingDate, @PathVariable boolean includeSubTaskgroups){
PermissionResult<Taskgroup> taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName());
if(!taskgroupPermissionResult.isHasPermissions()) {
return ResponseEntity.status(403).body(new SimpleStatusResponse("failed"));
} else if(taskgroupPermissionResult.getExitCode() == ServiceExitCode.MISSING_ENTITY) {
return ResponseEntity.status(404).body(new SimpleStatusResponse("failed"));
}
return ResponseEntity.ok(taskgroupPermissionResult.getResult().calcActivityInfo(includeSubTaskgroups,
LocalDate.parse(startingDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")), LocalDate.parse(endingDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"))));
}
} }

View File

@ -0,0 +1,20 @@
package core.api.models.timemanager.history;
import com.fasterxml.jackson.annotation.JsonProperty;
import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo;
import java.time.LocalDate;
public class TaskgroupActivityInfo {
@JsonProperty
private LocalDate date;
@JsonProperty
private int activeMinutes;
public TaskgroupActivityInfo(int activeMinutes, LocalDate localDate) {
this.date = localDate;
this.activeMinutes = activeMinutes;
}
}

View File

@ -9,13 +9,18 @@ import java.util.List;
public class TaskgroupPathInfo { public class TaskgroupPathInfo {
@JsonProperty @JsonProperty
private List<String> taskgroupPathNames; private String taskgroupPath;
@JsonProperty
private List<TaskgroupEntityInfo> directChildren;
public TaskgroupPathInfo(Taskgroup taskgroup) { public TaskgroupPathInfo(Taskgroup taskgroup) {
taskgroupPathNames = new ArrayList<>();
List<Taskgroup> taskgroupPath = Taskgroup.getAncestorList(taskgroup); List<Taskgroup> taskgroupPath = Taskgroup.getAncestorList(taskgroup);
for(Taskgroup cT : taskgroupPath) { StringBuilder stringBuilder = new StringBuilder();
taskgroupPathNames.add(cT.getTaskgroupName()); for(Taskgroup taskgroupPathEntity : taskgroupPath) {
} stringBuilder.append(taskgroupPathEntity.getTaskgroupName());
stringBuilder.append("/");
}
this.taskgroupPath = stringBuilder.substring(0, stringBuilder.length()-1);
directChildren = taskgroup.getChildren().stream().map(TaskgroupEntityInfo::new).toList();
} }
} }

View File

@ -93,6 +93,10 @@ public abstract class AbstractSchedule {
return startTime != null && stopTime == null; return startTime != null && stopTime == null;
} }
public boolean isCompleted() {
return startTime != null && stopTime != null;
}
public int getActiveTime() { public int getActiveTime() {
if(startTime == null) { if(startTime == null) {
return 0; return 0;

View File

@ -188,4 +188,14 @@ public class Task {
public void setFinishable(boolean finishable) { public void setFinishable(boolean finishable) {
this.finishable = finishable; this.finishable = finishable;
} }
public int calcOverallActivityInfo(LocalDate date) {
int activeMinutes = 0;
for(AbstractSchedule schedule : getBasicTaskSchedules()) {
if(schedule.isCompleted() && schedule.getStartTime().toLocalDate().isEqual(date)) {
activeMinutes += schedule.calcActiveMinutes();
}
}
return activeMinutes;
}
} }

View File

@ -1,9 +1,12 @@
package core.entities.timemanager; package core.entities.timemanager;
import core.api.models.timemanager.history.TaskgroupActivityInfo;
import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo;
import core.entities.User; import core.entities.User;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import java.time.LocalDate;
import java.util.*; import java.util.*;
@Entity @Entity
@ -132,4 +135,55 @@ public class Taskgroup {
public int hashCode() { public int hashCode() {
return Objects.hash(taskgroupID); return Objects.hash(taskgroupID);
} }
public List<TaskgroupActivityInfo> calcActivityInfo(boolean includeChildTasks, LocalDate start, LocalDate end) {
HashMap<LocalDate, Integer> activityInfos = new HashMap<>();
if(includeChildTasks) {
Queue<Taskgroup> queue = new LinkedList<>(children);
while(!queue.isEmpty()) {
Taskgroup childTraskgroup = queue.poll();
LocalDate currentDate = start;
while(!currentDate.isAfter(end)) {
int activeMinutes = 0;
for(Task task : childTraskgroup.getTasks()) {
activeMinutes += task.calcOverallActivityInfo(currentDate);
}
if(activityInfos.containsKey(currentDate)) {
activityInfos.put(currentDate, activityInfos.get(currentDate) + activeMinutes);
} else {
activityInfos.put(currentDate, activeMinutes);
}
currentDate = currentDate.plusDays(1);
}
queue.addAll(childTraskgroup.getChildren());
}
}
LocalDate currentDate = start;
while(!currentDate.isAfter(end)) {
int activeMinutes = 0;
for(Task task : tasks) {
activeMinutes += task.calcOverallActivityInfo(currentDate);
}
if(activityInfos.containsKey(currentDate)) {
activityInfos.put(currentDate, activityInfos.get(currentDate) + activeMinutes);
} else {
activityInfos.put(currentDate, activeMinutes);
}
currentDate = currentDate.plusDays(1);
}
List<TaskgroupActivityInfo> taskgroupActivityInfos = new ArrayList<>();
for(Map.Entry<LocalDate, Integer> entry : activityInfos.entrySet()) {
taskgroupActivityInfos.add(new TaskgroupActivityInfo(entry.getValue(), entry.getKey()));
}
return taskgroupActivityInfos;
}
} }

View File

@ -1,10 +1,13 @@
package core.repositories.timemanager; package core.repositories.timemanager;
import core.entities.timemanager.AbstractSchedule; import core.entities.timemanager.AbstractSchedule;
import core.entities.timemanager.Taskgroup;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;

View File

@ -9,9 +9,14 @@
* https://openapi-generator.tech * https://openapi-generator.tech
* Do not edit the class manually. * Do not edit the class manually.
*/ */
import { TaskgroupEntityInfo } from './taskgroupEntityInfo';
export interface TaskgroupPathInfo { export interface TaskgroupPathInfo {
taskgroupPathNames: Array<string>; /**
* TaskgroupPath
*/
taskgroupPath: string;
directChildren: Array<TaskgroupEntityInfo>;
} }

View File

@ -2,8 +2,8 @@
<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: 100%"> <mat-form-field style="width: 100%">
<mat-label>Toppings</mat-label> <mat-label>Toppings</mat-label>
<mat-select [(ngModel)]="selectedSeries" multiple (ngModelChange)="updateSerieSelection()"> <mat-select [(ngModel)]="selectedTaskgroupPath" (ngModelChange)="updateSerieSelection()">
<mat-option *ngFor="let topping of toppingList" [value]="topping">{{topping}}</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>

View File

@ -11,7 +11,7 @@ import {
} 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 {TaskgroupService} from "../../../api"; import {HistoryService, TaskgroupPathInfo, TaskgroupService} from "../../../api";
export type ChartOptions = { export type ChartOptions = {
series: ApexAxisChartSeries; series: ApexAxisChartSeries;
@ -41,9 +41,9 @@ export class TaskgroupActivityComponent implements OnInit{
]; ];
toppings = new FormControl();
toppingList: string[] = []; selectedTaskgroupPath: TaskgroupPathInfo | undefined
selectedSeries: string[] = [] taskgroupPaths: TaskgroupPathInfo[] = []
sliderControl: FormControl = new FormControl() sliderControl: FormControl = new FormControl()
dateRange: Date[] = this.createDateRange(); dateRange: Date[] = this.createDateRange();
selectedDateRange: Date[] = this.dateRange; selectedDateRange: Date[] = this.dateRange;
@ -62,17 +62,14 @@ export class TaskgroupActivityComponent implements OnInit{
@ViewChild("chart") chart?: ChartComponent; @ViewChild("chart") chart?: ChartComponent;
public chartOptions: Partial<ChartOptions> = this.generateChartOptions() public chartOptions: Partial<ChartOptions> = this.generateChartOptions()
constructor(private taskgroupService: TaskgroupService) { constructor(private taskgroupService: TaskgroupService,
private historyService: HistoryService) {
} }
ngOnInit() { ngOnInit() {
this.taskgroupService.taskgroupsPathGet().subscribe({ this.taskgroupService.taskgroupsPathGet().subscribe({
next: resp => { next: resp => {
resp.forEach(taskgroupPath => { this.taskgroupPaths = resp;
let taskgroupPathName = "";
taskgroupPath.taskgroupPathNames.forEach(name => taskgroupPathName += (name + "/"))
this.toppingList.push(taskgroupPathName)
})
} }
}) })
} }
@ -113,7 +110,7 @@ export class TaskgroupActivityComponent implements OnInit{
generateChartOptions(): Partial<ChartOptions> { generateChartOptions(): Partial<ChartOptions> {
return { return {
series: this.selectedSeries.map(serie => this.generateSeries(serie)!), series: this.generateSeries(),
chart: { chart: {
height: 350, height: 350,
type: "bar", type: "bar",
@ -128,28 +125,26 @@ export class TaskgroupActivityComponent implements OnInit{
}; };
} }
generateSeries(serie: string) { generateSeries() : ApexAxisChartSeries {
if(serie == "KIT/") { const series: ApexAxisChartSeries = []
return { this.selectedTaskgroupPath?.directChildren.forEach(taskgroup => {
name: "Marine Sprite", this.historyService.statisticsTaskgroupActivityTaskgroupIDStartingDateEndingDateIncludeSubTaskgroupsGet(
data: [44, 55, 41, 37, 22, 43, 21] taskgroup.taskgroupID,
} moment(this.selectedDateRange[0]).format("YYYY-MM-DD"),
} else if(serie == "Striking Calf") { moment(this.selectedDateRange[this.selectedDateRange.length-1]).format("YYYY-MM-DD"), true
return { ).subscribe({
name: "Striking Calf", next: resp => {
data: [53, 32, 33, 52, 13, 43, 32] series.push(
} {
} else if(serie == "Tank Picture") { name: taskgroup.taskgroupName,
return { data: resp.map(dailyActivityInfo => dailyActivityInfo.activeMinutes)
name: "Tank Picture",
data: [12, 17, 11, 9, 15, 11, 20]
}
} else {
return {
name: "",
data: []
} }
)
} }
})
})
return series;
} }

View File

@ -1905,6 +1905,63 @@ paths:
schema: schema:
type: object type: object
$ref: "#/components/schemas/SimpleStatusResponse" $ref: "#/components/schemas/SimpleStatusResponse"
/statistics/taskgroup-activity/{taskgroupID}/{startingDate}/{endingDate}/{includeSubTaskgroups}:
get:
security:
- API_TOKEN: []
tags:
- history
parameters:
- name: taskgroupID
in: path
description: internal id of taskgroup
required: true
schema:
type: number
example: 1
- name: startingDate
in: path
description: starting date
required: true
schema:
type: string
format: date
- name: endingDate
in: path
description: starting date
required: true
schema:
type: string
format: date
- name: includeSubTaskgroups
in: path
description: determines whether to include subtaskgroups or not
required: true
schema:
type: boolean
example: false
responses:
200:
description: Operation successfull
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/TaskgroupActivityInfo'
403:
description: No permission
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleStatusResponse'
404:
description: Schedule not found
content:
application/json:
schema:
$ref: '#/components/schemas/SimpleStatusResponse'
components: components:
securitySchemes: securitySchemes:
@ -2549,10 +2606,27 @@ components:
format: date format: date
TaskgroupPathInfo: TaskgroupPathInfo:
required: required:
- taskgroupPathNames - taskgroupPath
- directChildren
additionalProperties: false additionalProperties: false
properties: properties:
taskgroupPathNames: taskgroupPath:
type: string
description: TaskgroupPath
directChildren:
type: array type: array
items: items:
$ref: '#/components/schemas/TaskgroupEntityInfo'
TaskgroupActivityInfo:
required:
- date
- activeMinutes
additionalProperties: false
properties:
date:
type: string type: string
format: date
activeMinutes:
type: number
description: Number of minutes the task was active
example: 122