From 882169840d1c79dc24c9c1658f7b057b1cab90f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 19 Nov 2023 14:52:33 +0100 Subject: [PATCH] Build data of series by data provided through historyService --- .../api/controller/StatisticController.java | 31 ++++++- .../history/TaskgroupActivityInfo.java | 20 +++++ .../taskgroup/TaskgroupPathInfo.java | 13 ++- .../timemanager/AbstractSchedule.java | 4 + .../java/core/entities/timemanager/Task.java | 10 +++ .../core/entities/timemanager/Taskgroup.java | 54 +++++++++++++ .../timemanager/ScheduleRepository.java | 3 + frontend/src/api/model/taskgroupPathInfo.ts | 7 +- .../taskgroup-activity.component.html | 4 +- .../taskgroup-activity.component.ts | 61 +++++++------- openapi.yaml | 80 ++++++++++++++++++- 11 files changed, 240 insertions(+), 47 deletions(-) create mode 100644 backend/src/main/java/core/api/models/timemanager/history/TaskgroupActivityInfo.java diff --git a/backend/src/main/java/core/api/controller/StatisticController.java b/backend/src/main/java/core/api/controller/StatisticController.java index 5abfe02..9558db0 100644 --- a/backend/src/main/java/core/api/controller/StatisticController.java +++ b/backend/src/main/java/core/api/controller/StatisticController.java @@ -1,21 +1,30 @@ 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.taskgroup.TaskgroupEntityInfo; 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.TaskgroupService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.parameters.P; +import org.springframework.web.bind.annotation.*; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; +import java.util.Map; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -23,6 +32,7 @@ import java.util.List; public class StatisticController { @Autowired private TaskScheduleService taskScheduleService; + @Autowired private TaskgroupService taskgroupService; @GetMapping("/history/workingStatus") public ResponseEntity getWorkingStatus() { @@ -39,4 +49,17 @@ public class StatisticController { 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 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")))); + } } diff --git a/backend/src/main/java/core/api/models/timemanager/history/TaskgroupActivityInfo.java b/backend/src/main/java/core/api/models/timemanager/history/TaskgroupActivityInfo.java new file mode 100644 index 0000000..2441617 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/history/TaskgroupActivityInfo.java @@ -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; + } +} diff --git a/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupPathInfo.java b/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupPathInfo.java index e37be0a..2e5d9a4 100644 --- a/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupPathInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/taskgroup/TaskgroupPathInfo.java @@ -9,13 +9,18 @@ import java.util.List; public class TaskgroupPathInfo { @JsonProperty - private List taskgroupPathNames; + private String taskgroupPath; + @JsonProperty + private List directChildren; public TaskgroupPathInfo(Taskgroup taskgroup) { - taskgroupPathNames = new ArrayList<>(); List taskgroupPath = Taskgroup.getAncestorList(taskgroup); - for(Taskgroup cT : taskgroupPath) { - taskgroupPathNames.add(cT.getTaskgroupName()); + StringBuilder stringBuilder = new StringBuilder(); + 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(); } } diff --git a/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java b/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java index 4a07efb..f7f4de1 100644 --- a/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java +++ b/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java @@ -93,6 +93,10 @@ public abstract class AbstractSchedule { return startTime != null && stopTime == null; } + public boolean isCompleted() { + return startTime != null && stopTime != null; + } + public int getActiveTime() { if(startTime == null) { return 0; diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index c6bfe1d..1858ca8 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -188,4 +188,14 @@ public class Task { public void setFinishable(boolean 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; + } } diff --git a/backend/src/main/java/core/entities/timemanager/Taskgroup.java b/backend/src/main/java/core/entities/timemanager/Taskgroup.java index bdf42ca..016c607 100644 --- a/backend/src/main/java/core/entities/timemanager/Taskgroup.java +++ b/backend/src/main/java/core/entities/timemanager/Taskgroup.java @@ -1,9 +1,12 @@ package core.entities.timemanager; +import core.api.models.timemanager.history.TaskgroupActivityInfo; +import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo; import core.entities.User; import javax.persistence.*; import javax.validation.constraints.NotBlank; +import java.time.LocalDate; import java.util.*; @Entity @@ -132,4 +135,55 @@ public class Taskgroup { public int hashCode() { return Objects.hash(taskgroupID); } + + public List calcActivityInfo(boolean includeChildTasks, LocalDate start, LocalDate end) { + HashMap activityInfos = new HashMap<>(); + if(includeChildTasks) { + + Queue 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 taskgroupActivityInfos = new ArrayList<>(); + for(Map.Entry entry : activityInfos.entrySet()) { + taskgroupActivityInfos.add(new TaskgroupActivityInfo(entry.getValue(), entry.getKey())); + } + return taskgroupActivityInfos; + } } diff --git a/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java b/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java index 92ea8b6..df72a88 100644 --- a/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java @@ -1,10 +1,13 @@ package core.repositories.timemanager; import core.entities.timemanager.AbstractSchedule; +import core.entities.timemanager.Taskgroup; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.util.Collection; import java.util.List; import java.util.Optional; diff --git a/frontend/src/api/model/taskgroupPathInfo.ts b/frontend/src/api/model/taskgroupPathInfo.ts index 2c09be3..b13e526 100644 --- a/frontend/src/api/model/taskgroupPathInfo.ts +++ b/frontend/src/api/model/taskgroupPathInfo.ts @@ -9,9 +9,14 @@ * https://openapi-generator.tech * Do not edit the class manually. */ +import { TaskgroupEntityInfo } from './taskgroupEntityInfo'; export interface TaskgroupPathInfo { - taskgroupPathNames: Array; + /** + * TaskgroupPath + */ + taskgroupPath: string; + directChildren: Array; } diff --git a/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.html b/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.html index fa3d545..b4d43c1 100644 --- a/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.html +++ b/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.html @@ -2,8 +2,8 @@ Toppings - - {{topping}} + + {{topping.taskgroupPath}} diff --git a/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.ts b/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.ts index d2424a4..7cefd6d 100644 --- a/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.ts +++ b/frontend/src/app/statistics/taskgroup-activity/taskgroup-activity.component.ts @@ -11,7 +11,7 @@ import { } from "ng-apexcharts"; import {timeInterval} from "rxjs"; import {FormControl} from "@angular/forms"; -import {TaskgroupService} from "../../../api"; +import {HistoryService, TaskgroupPathInfo, TaskgroupService} from "../../../api"; export type ChartOptions = { series: ApexAxisChartSeries; @@ -41,9 +41,9 @@ export class TaskgroupActivityComponent implements OnInit{ ]; - toppings = new FormControl(); - toppingList: string[] = []; - selectedSeries: string[] = [] + + selectedTaskgroupPath: TaskgroupPathInfo | undefined + taskgroupPaths: TaskgroupPathInfo[] = [] sliderControl: FormControl = new FormControl() dateRange: Date[] = this.createDateRange(); selectedDateRange: Date[] = this.dateRange; @@ -62,17 +62,14 @@ export class TaskgroupActivityComponent implements OnInit{ @ViewChild("chart") chart?: ChartComponent; public chartOptions: Partial = this.generateChartOptions() - constructor(private taskgroupService: TaskgroupService) { + constructor(private taskgroupService: TaskgroupService, + private historyService: HistoryService) { } ngOnInit() { this.taskgroupService.taskgroupsPathGet().subscribe({ next: resp => { - resp.forEach(taskgroupPath => { - let taskgroupPathName = ""; - taskgroupPath.taskgroupPathNames.forEach(name => taskgroupPathName += (name + "/")) - this.toppingList.push(taskgroupPathName) - }) + this.taskgroupPaths = resp; } }) } @@ -113,7 +110,7 @@ export class TaskgroupActivityComponent implements OnInit{ generateChartOptions(): Partial { return { - series: this.selectedSeries.map(serie => this.generateSeries(serie)!), + series: this.generateSeries(), chart: { height: 350, type: "bar", @@ -128,28 +125,26 @@ export class TaskgroupActivityComponent implements OnInit{ }; } - generateSeries(serie: string) { - if(serie == "KIT/") { - return { - name: "Marine Sprite", - data: [44, 55, 41, 37, 22, 43, 21] - } - } else if(serie == "Striking Calf") { - return { - name: "Striking Calf", - data: [53, 32, 33, 52, 13, 43, 32] - } - } else if(serie == "Tank Picture") { - return { - name: "Tank Picture", - data: [12, 17, 11, 9, 15, 11, 20] - } - } else { - return { - name: "", - data: [] - } - } + 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; } diff --git a/openapi.yaml b/openapi.yaml index 25f2353..6c8bf09 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1905,6 +1905,63 @@ paths: schema: type: object $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: securitySchemes: @@ -2549,10 +2606,27 @@ components: format: date TaskgroupPathInfo: required: - - taskgroupPathNames + - taskgroupPath + - directChildren additionalProperties: false properties: - taskgroupPathNames: + taskgroupPath: + type: string + description: TaskgroupPath + directChildren: type: array items: - type: string \ No newline at end of file + $ref: '#/components/schemas/TaskgroupEntityInfo' + TaskgroupActivityInfo: + required: + - date + - activeMinutes + additionalProperties: false + properties: + date: + type: string + format: date + activeMinutes: + type: number + description: Number of minutes the task was active + example: 122 \ No newline at end of file