From 160c379116a4a516ff783011aae5833cf2405cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 10:33:35 +0100 Subject: [PATCH 01/18] Adapt Task to consider subtasks and implement service to create Subtasks --- .../java/core/entities/timemanager/Task.java | 28 +++++++++++++++++++ .../main/java/core/services/TaskService.java | 16 ++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index dbb92c5..b66b9c5 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -33,6 +33,13 @@ public class Task { @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) private TaskSerieItem taskSerieItem; + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private Set subtasks; + + @ManyToOne + @JoinColumn(name = "parent") + private Task parent; + public Task() { this.basicTaskSchedules = new ArrayList<>(); } @@ -149,6 +156,27 @@ public class Task { this.basicTaskSchedules = basicTaskSchedules; } + public void addSubtask(Task subtask) { + subtask.setParent(this); + this.subtasks.add(subtask); + } + + public Set getSubtasks() { + return subtasks; + } + + public void setSubtasks(Set subtasks) { + this.subtasks = subtasks; + } + + public Task getParent() { + return parent; + } + + public void setParent(Task parent) { + this.parent = parent; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index 292b442..2074798 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -49,6 +49,21 @@ public class TaskService { return new ServiceResult<>(task); } + public ServiceResult createSubTask(Task parentTask, TaskFieldInfo taskFieldInfo) { + if(taskFieldInfo.getStartDate().isBefore(parentTask.getStartDate()) || + taskFieldInfo.getDeadline().isAfter(parentTask.getDeadline()) || + taskFieldInfo.getDeadline().isBefore(taskFieldInfo.getStartDate())) { + return new ServiceResult<>(ServiceExitCode.INVALID_PARAMETER); + } + + Task task = new Task(parentTask.getTaskgroup(), taskFieldInfo); + parentTask.getTaskgroup().getTasks().add(task); + parentTask.addSubtask(task); + + taskRepository.save(task); + return new ServiceResult<>(task); + } + private boolean existTaskByName(Collection tasks, String name) { for(Task task : tasks) { if(task.getTaskName().equals(name)) { @@ -94,7 +109,6 @@ public class TaskService { taskSeriesService.deleteTaskSeriesItem(task); } taskRepository.delete(task); - } public void clearTasks(Taskgroup taskgroup) { From d7b2683ffce23ed161b003e5801f939f7b8f63f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 10:57:50 +0100 Subject: [PATCH 02/18] Ui Support for Creating Subtasks --- .../core/api/controller/TaskController.java | 16 +++++ .../java/core/entities/timemanager/Task.java | 2 +- frontend/src/api/api/task.service.ts | 70 +++++++++++++++++++ .../active-task-overview.component.ts | 3 +- .../task-overview/task-overview.component.ts | 3 +- .../taskgroup-dashboard.component.ts | 3 +- .../task-dashboard.component.ts | 3 +- .../task-detail-overview.component.html | 2 +- .../task-detail-overview.component.ts | 15 +++- .../app/tasks/task-editor/TaskEditorData.ts | 1 + .../task-editor/task-editor.component.html | 3 +- .../task-editor/task-editor.component.ts | 37 +++++++++- .../upcoming-task-overview.component.ts | 3 +- openapi.yaml | 47 +++++++++++++ 14 files changed, 197 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/core/api/controller/TaskController.java b/backend/src/main/java/core/api/controller/TaskController.java index 823e1aa..408f6f7 100644 --- a/backend/src/main/java/core/api/controller/TaskController.java +++ b/backend/src/main/java/core/api/controller/TaskController.java @@ -164,4 +164,20 @@ public class TaskController { taskService.finishTask(taskPermissionResult.getResult()); return ResponseEntity.ok(new SimpleStatusResponse("success")); } + + @PutMapping("/tasks/{taskID}/createSubtask") + public ResponseEntity onCreateSubTask(@PathVariable long taskID, @Valid @RequestBody TaskFieldInfo taskFieldInfo) { + var taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(taskPermissionResult.hasIssue()) { + return taskPermissionResult.mapToResponseEntity(); + } + + var serviceResult = taskService.createSubTask(taskPermissionResult.getResult(), taskFieldInfo); + if(serviceResult.hasIssue()) { + return serviceResult.mapToResponseEntity(); + } else { + return ResponseEntity.ok(new TaskEntityInfo(serviceResult.getResult())); + } + + } } diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index b66b9c5..c1a8d0f 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -33,7 +33,7 @@ public class Task { @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) private TaskSerieItem taskSerieItem; - @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) private Set subtasks; @ManyToOne diff --git a/frontend/src/api/api/task.service.ts b/frontend/src/api/api/task.service.ts index d34276b..2f417ef 100644 --- a/frontend/src/api/api/task.service.ts +++ b/frontend/src/api/api/task.service.ts @@ -153,6 +153,76 @@ export class TaskService { ); } + /** + * Creates Subtask + * Create Subtask + * @param taskID internal id of task + * @param taskFieldInfo + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + if (taskID === null || taskID === undefined) { + throw new Error('Required parameter taskID was null or undefined when calling tasksTaskIDCreateSubtaskPut.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (API_TOKEN) required + localVarCredential = this.configuration.lookupCredential('API_TOKEN'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' = 'json'; + if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } + + return this.httpClient.put(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/createSubtask`, + taskFieldInfo, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * edits an existing task * edits an existing task diff --git a/frontend/src/app/active-task-overview/active-task-overview.component.ts b/frontend/src/app/active-task-overview/active-task-overview.component.ts index c945579..c82a11e 100644 --- a/frontend/src/app/active-task-overview/active-task-overview.component.ts +++ b/frontend/src/app/active-task-overview/active-task-overview.component.ts @@ -93,7 +93,8 @@ export class ActiveTaskOverviewComponent implements OnInit{ editTask(editedTask: TaskTaskgroupInfo) { const taskEditorInfo: TaskEditorData = { task: editedTask, - taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID + taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID, + parentTask: undefined }; this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, width: "600px"}) } diff --git a/frontend/src/app/dashboard/task-overview/task-overview.component.ts b/frontend/src/app/dashboard/task-overview/task-overview.component.ts index ef54dca..6c19c3f 100644 --- a/frontend/src/app/dashboard/task-overview/task-overview.component.ts +++ b/frontend/src/app/dashboard/task-overview/task-overview.component.ts @@ -75,7 +75,8 @@ export class TaskOverviewComponent { openTaskCreation() { const editorData: TaskEditorData = { task: undefined, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID!, + parentTask: undefined } const dialogRef = this.dialog.open(TaskEditorComponent, {data: editorData, width: "600px"}) dialogRef.afterClosed().subscribe(res => { diff --git a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts index 79835b5..8fead1a 100644 --- a/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts +++ b/frontend/src/app/taskgroups/taskgroup-dashboard/taskgroup-dashboard.component.ts @@ -104,7 +104,8 @@ export class TaskgroupDashboardComponent implements OnInit { openTaskCreation() { const editorData: TaskEditorData = { task: undefined, - taskgroupID: this.taskgroupID + taskgroupID: this.taskgroupID, + parentTask: undefined } const dialogRef = this.dialog.open(TaskEditorComponent, {data: editorData, width: "600px"}) dialogRef.afterClosed().subscribe(res => { diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts index 49b5e14..21e8949 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -97,7 +97,8 @@ export class TaskDashboardComponent implements OnChanges{ editTask(task: TaskEntityInfo) { const taskEditorInfo: TaskEditorData = { task: task, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID!, + parentTask: undefined }; const dialogRef = this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, minWidth: "400px"}) } diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html index 24038f3..90577ea 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html @@ -7,7 +7,7 @@
{{taskStatus + " " + task!.taskName}}
- + diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts index d557009..66259bd 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts @@ -135,7 +135,8 @@ export class TaskDetailOverviewComponent implements OnInit { if(this.task != undefined) { const taskEditorInfo: TaskEditorData = { task: this.task!, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID!, + parentTask: undefined }; this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, width: "600px"}) } @@ -166,4 +167,16 @@ export class TaskDetailOverviewComponent implements OnInit { minWidth: "400px" }) } + + addSubtask() { + const editorData: TaskEditorData = { + task: undefined, + taskgroupID: this.taskgroupID, + parentTask: this.task + } + const dialogRef = this.dialog.open(TaskEditorComponent, { + data: editorData, + minWidth: "400px" + }) + } } diff --git a/frontend/src/app/tasks/task-editor/TaskEditorData.ts b/frontend/src/app/tasks/task-editor/TaskEditorData.ts index f1140e2..30ddcc7 100644 --- a/frontend/src/app/tasks/task-editor/TaskEditorData.ts +++ b/frontend/src/app/tasks/task-editor/TaskEditorData.ts @@ -3,4 +3,5 @@ import {TaskEntityInfo, TaskTaskgroupInfo} from "../../../api"; export interface TaskEditorData { taskgroupID: number; task: TaskTaskgroupInfo | TaskEntityInfo | undefined + parentTask: TaskEntityInfo | undefined } diff --git a/frontend/src/app/tasks/task-editor/task-editor.component.html b/frontend/src/app/tasks/task-editor/task-editor.component.html index c413dff..1731ac0 100644 --- a/frontend/src/app/tasks/task-editor/task-editor.component.html +++ b/frontend/src/app/tasks/task-editor/task-editor.component.html @@ -1,6 +1,7 @@

Edit Task ({{editorData.task!.taskName}})

-

Create New Task

+

Create New {{editorData.parentTask != undefined? 'Sub-':''}}Task

+

Create a new Subtask for the Task {{editorData.parentTask!.taskName}}

Name diff --git a/frontend/src/app/tasks/task-editor/task-editor.component.ts b/frontend/src/app/tasks/task-editor/task-editor.component.ts index 6c4eb37..5eacda2 100644 --- a/frontend/src/app/tasks/task-editor/task-editor.component.ts +++ b/frontend/src/app/tasks/task-editor/task-editor.component.ts @@ -61,7 +61,36 @@ export class TaskEditorComponent implements OnInit { startDate_formatted = moment(this.startDate.value).format('YYYY-MM-DDTHH:mm:ss.SSSZ'); } - this.taskService.tasksTaskgroupIDPut(this.editorData.taskgroupID, { + if(this.editorData.parentTask != undefined) { + this.createSubTask(startDate_formatted, endDate_formatted); + } else { + this.taskService.tasksTaskgroupIDPut(this.editorData.taskgroupID, { + taskName: this.nameCtrl.value, + eta: this.etaCtrl.value, + startDate: startDate_formatted, + deadline: endDate_formatted, + finishable: this.finishable, + }).subscribe({ + next: resp => { + this.dialog.close(resp); + }, + error: err => { + if(err.status == 403) { + this.snackbar.open("No permission", "", {duration: 2000}); + } else if(err.status == 404) { + this.snackbar.open("Taskgroup not found", "", {duration: 2000}); + } else if(err.status == 409) { + this.snackbar.open("Task already exists", "", {duration: 2000}); + } else { + this.snackbar.open("Unexpected error", "", {duration: 3000}); + } + } + }) + } + } + + createSubTask(startDate_formatted: string|undefined, endDate_formatted: string|undefined) { + this.taskService.tasksTaskIDCreateSubtaskPut(this.editorData.parentTask!.taskID, { taskName: this.nameCtrl.value, eta: this.etaCtrl.value, startDate: startDate_formatted, @@ -78,7 +107,9 @@ export class TaskEditorComponent implements OnInit { this.snackbar.open("Taskgroup not found", "", {duration: 2000}); } else if(err.status == 409) { this.snackbar.open("Task already exists", "", {duration: 2000}); - } else { + } else if(err.status == 400) { + this.snackbar.open("Invalid Dates", "", {duration: 2000}) + } else { this.snackbar.open("Unexpected error", "", {duration: 3000}); } } @@ -123,4 +154,6 @@ export class TaskEditorComponent implements OnInit { } }) } + + protected readonly parent = parent; } diff --git a/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts b/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts index b022b24..d1e5891 100644 --- a/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts +++ b/frontend/src/app/upcoming-task-overview/upcoming-task-overview.component.ts @@ -59,7 +59,8 @@ export class UpcomingTaskOverviewComponent implements OnInit{ editTask(editedTask: TaskTaskgroupInfo) { const taskEditorInfo: TaskEditorData = { task: editedTask, - taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID + taskgroupID: editedTask.taskgroups[editedTask.taskgroups.length-1].taskgroupID, + parentTask: undefined }; this.dialog.open(TaskEditorComponent, {data: taskEditorInfo, width: "600px"}) } diff --git a/openapi.yaml b/openapi.yaml index 2ff5ea4..c3eb30e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1269,6 +1269,53 @@ paths: schema: type: object $ref: "#/components/schemas/SimpleStatusResponse" + /tasks/{taskID}/createSubtask: + put: + security: + - API_TOKEN: [] + tags: + - task + description: Create Subtask + summary: Creates Subtask + parameters: + - name: taskID + in: path + description: internal id of task + required: true + schema: + type: number + example: 1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TaskFieldInfo' + responses: + 200: + description: Operation successfull + content: + application/json: + schema: + $ref: '#/components/schemas/TaskEntityInfo' + 403: + description: No permission + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 404: + description: Task not found + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 400: + description: Invalid start/end date + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + /schedules: get: security: From 682d88e183b895720221506777040c40fc337529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 11:10:29 +0100 Subject: [PATCH 03/18] Remove Subtasks from TaskDashboard --- .../src/main/java/core/api/controller/TaskController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/core/api/controller/TaskController.java b/backend/src/main/java/core/api/controller/TaskController.java index 408f6f7..f982187 100644 --- a/backend/src/main/java/core/api/controller/TaskController.java +++ b/backend/src/main/java/core/api/controller/TaskController.java @@ -72,7 +72,11 @@ public class TaskController { return ResponseEntity.status(404).body(new SimpleStatusResponse("failed")); } - return ResponseEntity.ok(taskgroupPermissionResult.getResult().getTasks().stream().map(TaskEntityInfo::new)); + List tasks = new ArrayList<>(); + for(Task task : taskgroupPermissionResult.getResult().getTasks()) { + if(task.getParent() == null) tasks.add(task); + } + return ResponseEntity.ok(tasks.stream().map(TaskEntityInfo::new)); } @PutMapping("/tasks/{taskgroupID}") From 6047f4097c5ef56413968327621d9976afc3a3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 11:29:55 +0100 Subject: [PATCH 04/18] Display Subtasks in TaskDetail Component --- .../core/api/controller/TaskController.java | 14 +- frontend/src/api/api/task.service.ts | 197 +++++++++++------- .../task-dashboard.component.ts | 8 +- .../task-detail-overview.component.html | 7 + .../task-detail-overview.component.ts | 8 + .../task-editor/task-editor.component.ts | 2 +- openapi.yaml | 38 +++- 7 files changed, 200 insertions(+), 74 deletions(-) diff --git a/backend/src/main/java/core/api/controller/TaskController.java b/backend/src/main/java/core/api/controller/TaskController.java index f982187..c40c660 100644 --- a/backend/src/main/java/core/api/controller/TaskController.java +++ b/backend/src/main/java/core/api/controller/TaskController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collection; import java.util.List; @CrossOrigin(origins = "*", maxAge = 3600) @@ -169,7 +170,7 @@ public class TaskController { return ResponseEntity.ok(new SimpleStatusResponse("success")); } - @PutMapping("/tasks/{taskID}/createSubtask") + @PutMapping("/tasks/{taskID}/subtasks") public ResponseEntity onCreateSubTask(@PathVariable long taskID, @Valid @RequestBody TaskFieldInfo taskFieldInfo) { var taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); if(taskPermissionResult.hasIssue()) { @@ -184,4 +185,15 @@ public class TaskController { } } + + @GetMapping("/tasks/{taskID}/subtasks") + public ResponseEntity onListSubtasks(@PathVariable long taskID) { + var taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(taskPermissionResult.hasIssue()) { + return taskPermissionResult.mapToResponseEntity(); + } + + Collection subtasks = taskPermissionResult.getResult().getSubtasks(); + return ResponseEntity.ok(subtasks.stream().map(TaskEntityInfo::new).toList()); + } } diff --git a/frontend/src/api/api/task.service.ts b/frontend/src/api/api/task.service.ts index 2f417ef..c15e918 100644 --- a/frontend/src/api/api/task.service.ts +++ b/frontend/src/api/api/task.service.ts @@ -153,76 +153,6 @@ export class TaskService { ); } - /** - * Creates Subtask - * Create Subtask - * @param taskID internal id of task - * @param taskFieldInfo - * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. - * @param reportProgress flag to report request and response progress. - */ - public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; - public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; - public tasksTaskIDCreateSubtaskPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { - if (taskID === null || taskID === undefined) { - throw new Error('Required parameter taskID was null or undefined when calling tasksTaskIDCreateSubtaskPut.'); - } - - let localVarHeaders = this.defaultHeaders; - - let localVarCredential: string | undefined; - // authentication (API_TOKEN) required - localVarCredential = this.configuration.lookupCredential('API_TOKEN'); - if (localVarCredential) { - localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); - } - - let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; - if (localVarHttpHeaderAcceptSelected === undefined) { - // to determine the Accept header - const httpHeaderAccepts: string[] = [ - 'application/json' - ]; - localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); - } - if (localVarHttpHeaderAcceptSelected !== undefined) { - localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); - } - - let localVarHttpContext: HttpContext | undefined = options && options.context; - if (localVarHttpContext === undefined) { - localVarHttpContext = new HttpContext(); - } - - - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); - } - - let responseType_: 'text' | 'json' = 'json'; - if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { - responseType_ = 'text'; - } - - return this.httpClient.put(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/createSubtask`, - taskFieldInfo, - { - context: localVarHttpContext, - responseType: responseType_, - withCredentials: this.configuration.withCredentials, - headers: localVarHeaders, - observe: observe, - reportProgress: reportProgress - } - ); - } - /** * edits an existing task * edits an existing task @@ -471,6 +401,133 @@ export class TaskService { ); } + /** + * @param taskID internal id of task + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public tasksTaskIDSubtasksGet(taskID: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDSubtasksGet(taskID: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; + public tasksTaskIDSubtasksGet(taskID: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>>; + public tasksTaskIDSubtasksGet(taskID: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + if (taskID === null || taskID === undefined) { + throw new Error('Required parameter taskID was null or undefined when calling tasksTaskIDSubtasksGet.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (API_TOKEN) required + localVarCredential = this.configuration.lookupCredential('API_TOKEN'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let responseType_: 'text' | 'json' = 'json'; + if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } + + return this.httpClient.get>(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/subtasks`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + + /** + * Creates Subtask + * Create Subtask + * @param taskID internal id of task + * @param taskFieldInfo + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public tasksTaskIDSubtasksPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskIDSubtasksPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDSubtasksPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDSubtasksPut(taskID: number, taskFieldInfo?: TaskFieldInfo, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + if (taskID === null || taskID === undefined) { + throw new Error('Required parameter taskID was null or undefined when calling tasksTaskIDSubtasksPut.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (API_TOKEN) required + localVarCredential = this.configuration.lookupCredential('API_TOKEN'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' = 'json'; + if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } + + return this.httpClient.put(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/subtasks`, + taskFieldInfo, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * creates a new task * creates tasks diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts index 21e8949..8737120 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnChanges, ViewChild} from '@angular/core'; +import {Component, Input, OnChanges, OnInit, ViewChild} from '@angular/core'; import {TaskEntityInfo, TaskService} from "../../../api"; import {MatPaginator} from "@angular/material/paginator"; import {MatSort} from "@angular/material/sort"; @@ -22,10 +22,15 @@ export class TaskDashboardComponent implements OnChanges{ ngOnChanges(): void { if(this.taskgroupID != undefined) { this.fetchTasks() + } else if(this.subTasks.length > 0) { + this.datasource.data = this.subTasks; + this.datasource.paginator = this.paginator!; + this.datasource.sort = this.sort!; } } @Input("taskgroupID") taskgroupID: number | undefined + @Input("subTasks") subTasks: TaskEntityInfo[] = [] @ViewChild(MatPaginator) paginator: MatPaginator | undefined @ViewChild(MatSort) sort: MatSort | undefined @@ -147,5 +152,6 @@ export class TaskDashboardComponent implements OnChanges{ resp.forEach(task => console.log(task)) } }) + } } diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html index 90577ea..92b3840 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html @@ -42,6 +42,13 @@ + + Subtasks +
+ +
+
+ Schedules diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts index 66259bd..369634e 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.ts @@ -50,6 +50,8 @@ export class TaskDetailOverviewComponent implements OnInit { currentProgress: string = "0"; futureProgress: string = "0"; + subTasks: TaskEntityInfo[] = []; + constructor(private activatedRoute: ActivatedRoute, private taskgroupService: TaskgroupService, private taskService: TaskService, @@ -100,6 +102,12 @@ export class TaskDetailOverviewComponent implements OnInit { this.calcProgress(); } }) + + this.taskService.tasksTaskIDSubtasksGet(Number(params.get('taskID'))).subscribe({ + next: resp => { + this.subTasks = resp; + } + }) } }); } diff --git a/frontend/src/app/tasks/task-editor/task-editor.component.ts b/frontend/src/app/tasks/task-editor/task-editor.component.ts index 5eacda2..ff12d58 100644 --- a/frontend/src/app/tasks/task-editor/task-editor.component.ts +++ b/frontend/src/app/tasks/task-editor/task-editor.component.ts @@ -90,7 +90,7 @@ export class TaskEditorComponent implements OnInit { } createSubTask(startDate_formatted: string|undefined, endDate_formatted: string|undefined) { - this.taskService.tasksTaskIDCreateSubtaskPut(this.editorData.parentTask!.taskID, { + this.taskService.tasksTaskIDSubtasksPut(this.editorData.parentTask!.taskID, { taskName: this.nameCtrl.value, eta: this.etaCtrl.value, startDate: startDate_formatted, diff --git a/openapi.yaml b/openapi.yaml index c3eb30e..23db524 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1269,7 +1269,7 @@ paths: schema: type: object $ref: "#/components/schemas/SimpleStatusResponse" - /tasks/{taskID}/createSubtask: + /tasks/{taskID}/subtasks: put: security: - API_TOKEN: [] @@ -1315,6 +1315,42 @@ paths: application/json: schema: $ref: '#/components/schemas/SimpleStatusResponse' + get: + security: + - API_TOKEN: [] + tags: + - task + parameters: + - name: taskID + in: path + description: internal id of task + required: true + schema: + type: number + example: 1 + responses: + 200: + description: Operation successfull + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/TaskEntityInfo' + 403: + description: No permission + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 404: + description: Task not found + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + + /schedules: get: From 7526b6c13407bad2f9d13b0ceb66e074c2f44443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 14:01:10 +0100 Subject: [PATCH 05/18] Hide copy function for subtasks (weekly) --- .../src/app/tasks/task-dashboard/task-dashboard.component.html | 2 +- .../src/app/tasks/task-dashboard/task-dashboard.component.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html index 9d04aaa..9f3319f 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html @@ -63,7 +63,7 @@ - + diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts index 8737120..a961599 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -26,6 +26,8 @@ export class TaskDashboardComponent implements OnChanges{ this.datasource.data = this.subTasks; this.datasource.paginator = this.paginator!; this.datasource.sort = this.sort!; + + this.displayedColumns = this.displayedColumns.filter(col => col !== 'select') } } From 8487a573a1ad4b193f092a4ca4cc039ed3c4c814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 14:04:57 +0100 Subject: [PATCH 06/18] Hide copy function for subtasks (daily) --- .../timemanager/tasks/TaskEntityInfo.java | 20 +++++++++++++++++++ frontend/src/api/model/taskEntityInfo.ts | 8 ++++++++ .../task-detail-overview.component.html | 2 +- openapi.yaml | 10 ++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java index bd5e11e..28404a9 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java @@ -29,6 +29,8 @@ public class TaskEntityInfo { private boolean hasPlannedSchedules; private boolean hasTaskSerie; + private boolean hasSubtasks; + private boolean hasParent; public TaskEntityInfo(Task task) { this.taskID = task.getTaskID(); @@ -49,6 +51,8 @@ public class TaskEntityInfo { this.hasActiveSchedules = task.hasActiveSchedule(); this.hasPlannedSchedules = task.hasPlannedSchedules(); this.hasTaskSerie = task.getTaskSerieItem() != null; + this.hasSubtasks = !task.getSubtasks().isEmpty(); + this.hasParent = task.getParent() != null; } public long getTaskID() { @@ -146,4 +150,20 @@ public class TaskEntityInfo { public void setHasTaskSerie(boolean hasTaskSerie) { this.hasTaskSerie = hasTaskSerie; } + + public boolean isHasSubtasks() { + return hasSubtasks; + } + + public void setHasSubtasks(boolean hasSubtasks) { + this.hasSubtasks = hasSubtasks; + } + + public boolean isHasParent() { + return hasParent; + } + + public void setHasParent(boolean hasParent) { + this.hasParent = hasParent; + } } diff --git a/frontend/src/api/model/taskEntityInfo.ts b/frontend/src/api/model/taskEntityInfo.ts index 3a2b2d0..5e18782 100644 --- a/frontend/src/api/model/taskEntityInfo.ts +++ b/frontend/src/api/model/taskEntityInfo.ts @@ -60,5 +60,13 @@ export interface TaskEntityInfo { * determines whether the task is associated with a taskserie */ hasTaskSerie: boolean; + /** + * determines whether a task has subtasks + */ + hasSubtasks: boolean; + /** + * determines whether a task is a top task or Not + */ + hasParent: boolean; } diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html index 92b3840..c530aad 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html @@ -29,7 +29,7 @@ - +
diff --git a/openapi.yaml b/openapi.yaml index 23db524..5af759a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2498,6 +2498,8 @@ components: - hasActiveSchedules - hasPlannedSchedules - hasTaskSerie + - hasSubtasks + - hasParent additionalProperties: false properties: taskID: @@ -2546,6 +2548,14 @@ components: type: boolean description: determines whether the task is associated with a taskserie example: false + hasSubtasks: + type: boolean + description: determines whether a task has subtasks + example: true + hasParent: + type: boolean + description: determines whether a task is a top task or Not + example: false TaskTaskgroupInfo: required: - taskID From dfe12e61022fa046b9ef59f4fabbf4d5cef20f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 14:18:23 +0100 Subject: [PATCH 07/18] Take subtasks in Dashboard into account (TaskOverview) --- .../api/models/timemanager/tasks/TaskOverviewInfo.java | 10 ++++++++++ frontend/src/api/model/taskOverviewInfo.ts | 4 ++++ .../task-overview/task-overview.component.css | 10 ++++++++++ .../task-overview/task-overview.component.html | 5 ++++- .../dashboard/task-overview/task-overview.component.ts | 1 + openapi.yaml | 4 ++++ 6 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/TaskOverviewInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/TaskOverviewInfo.java index 18ec648..227e93e 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/TaskOverviewInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/TaskOverviewInfo.java @@ -18,6 +18,7 @@ public class TaskOverviewInfo { private boolean overdue; private boolean finishable; + private boolean hasSubtasks; public TaskOverviewInfo(Task task) { this.taskID = task.getTaskID(); @@ -33,6 +34,7 @@ public class TaskOverviewInfo { this.overdue = LocalDate.now().isAfter(task.getDeadline()); } this.finishable = task.isFinishable(); + this.hasSubtasks = !task.getSubtasks().isEmpty(); } public long getTaskID() { @@ -90,4 +92,12 @@ public class TaskOverviewInfo { public void setFinishable(boolean finishable) { this.finishable = finishable; } + + public boolean isHasSubtasks() { + return hasSubtasks; + } + + public void setHasSubtasks(boolean hasSubtasks) { + this.hasSubtasks = hasSubtasks; + } } diff --git a/frontend/src/api/model/taskOverviewInfo.ts b/frontend/src/api/model/taskOverviewInfo.ts index 6fe5cbe..9929798 100644 --- a/frontend/src/api/model/taskOverviewInfo.ts +++ b/frontend/src/api/model/taskOverviewInfo.ts @@ -42,5 +42,9 @@ export interface TaskOverviewInfo { * determines whether the task can be finished */ finishable: boolean; + /** + * determines whether the task has subtasks + */ + hasSubtasks: boolean; } diff --git a/frontend/src/app/dashboard/task-overview/task-overview.component.css b/frontend/src/app/dashboard/task-overview/task-overview.component.css index 8c786e8..498becd 100644 --- a/frontend/src/app/dashboard/task-overview/task-overview.component.css +++ b/frontend/src/app/dashboard/task-overview/task-overview.component.css @@ -31,3 +31,13 @@ text-decoration: none; color: black; } + +.task-title { + display: flex; + justify-content: space-between; +} + +.subtask-link { + color: #00bc8c; + text-decoration: none; +} diff --git a/frontend/src/app/dashboard/task-overview/task-overview.component.html b/frontend/src/app/dashboard/task-overview/task-overview.component.html index 07c8e49..5ced071 100644 --- a/frontend/src/app/dashboard/task-overview/task-overview.component.html +++ b/frontend/src/app/dashboard/task-overview/task-overview.component.html @@ -2,7 +2,10 @@ -

{{task.taskName}}

+

+ {{task.taskName}} + +

ETA: {{task.activeTime}} / {{task.eta}}

Limit: {{task.limit}}

diff --git a/frontend/src/app/dashboard/task-overview/task-overview.component.ts b/frontend/src/app/dashboard/task-overview/task-overview.component.ts index 6c19c3f..fe4c8ae 100644 --- a/frontend/src/app/dashboard/task-overview/task-overview.component.ts +++ b/frontend/src/app/dashboard/task-overview/task-overview.component.ts @@ -89,6 +89,7 @@ export class TaskOverviewComponent { activeTime: 0, overdue: res.overdue, taskgroupPath: [], + hasSubtasks: false, finishable: res.finishable } this.creationEmitter.emit({ diff --git a/openapi.yaml b/openapi.yaml index 5af759a..1abccaa 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2807,6 +2807,7 @@ components: - overdue - taskgroupPath - finishable + - hasSubtasks additionalProperties: false properties: taskID: @@ -2841,6 +2842,9 @@ components: finishable: type: boolean description: determines whether the task can be finished + hasSubtasks: + type: boolean + description: determines whether the task has subtasks ScheduleStatus: required: - activeMinutes From 125ab10a00c4bc7b065c36bc738d70fa2200830a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 14:18:43 +0100 Subject: [PATCH 08/18] Deliver Task only if it is no subtask to TaskOverview --- .../timemanager/taskgroup/RecursiveTaskgroupInfo.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/core/api/models/timemanager/taskgroup/RecursiveTaskgroupInfo.java b/backend/src/main/java/core/api/models/timemanager/taskgroup/RecursiveTaskgroupInfo.java index 3e5e8e5..f1da472 100644 --- a/backend/src/main/java/core/api/models/timemanager/taskgroup/RecursiveTaskgroupInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/taskgroup/RecursiveTaskgroupInfo.java @@ -31,10 +31,13 @@ public class RecursiveTaskgroupInfo { } for(Task task : taskgroup.getActiveTasks()) { - this.activeTasks.add(new TaskOverviewInfo(task)); - if(task.getDeadline() != null && task.getDeadline().isBefore(LocalDate.now())) { - this.hasOverdueTask = true; + if(task.getParent() == null) { + this.activeTasks.add(new TaskOverviewInfo(task)); + if(task.getDeadline() != null && task.getDeadline().isBefore(LocalDate.now())) { + this.hasOverdueTask = true; + } } + } this.amountActiveTasks = taskgroup.getAmountOfActiveTasks(); } From dcdba67f226bc16dd765f99ccaf729e66663fcf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 14:28:25 +0100 Subject: [PATCH 09/18] Delete Tasks by Taskgroup and Fix NullpointerException in TaskEntityInfo Constructor --- .../api/models/timemanager/tasks/TaskEntityInfo.java | 2 +- .../repositories/timemanager/ScheduleRepository.java | 7 +++++++ .../src/main/java/core/services/TaskScheduleService.java | 9 +++++---- backend/src/main/java/core/services/TaskService.java | 1 + .../src/main/java/core/services/TaskgroupService.java | 5 ++++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java index 28404a9..e322b60 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java @@ -51,7 +51,7 @@ public class TaskEntityInfo { this.hasActiveSchedules = task.hasActiveSchedule(); this.hasPlannedSchedules = task.hasPlannedSchedules(); this.hasTaskSerie = task.getTaskSerieItem() != null; - this.hasSubtasks = !task.getSubtasks().isEmpty(); + this.hasSubtasks = task.getSubtasks() != null && !task.getSubtasks().isEmpty(); this.hasParent = task.getParent() != null; } diff --git a/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java b/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java index df72a88..da5e43a 100644 --- a/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/ScheduleRepository.java @@ -2,10 +2,12 @@ package core.repositories.timemanager; import core.entities.timemanager.AbstractSchedule; import core.entities.timemanager.Taskgroup; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import javax.transaction.Transactional; import java.time.LocalDate; import java.util.Collection; import java.util.List; @@ -20,4 +22,9 @@ public interface ScheduleRepository extends CrudRepository getActiveScheduleOfUser(String username); + + @Modifying + @Transactional + @Query(value = "DELETE FROM AbstractSchedule a WHERE a.task IN (SELECT t FROM Task t WHERE t.taskgroup = ?1)") + void deleteByTaskgroup(Taskgroup taskgroup); } diff --git a/backend/src/main/java/core/services/TaskScheduleService.java b/backend/src/main/java/core/services/TaskScheduleService.java index 57ed6d2..94c8df0 100644 --- a/backend/src/main/java/core/services/TaskScheduleService.java +++ b/backend/src/main/java/core/services/TaskScheduleService.java @@ -7,10 +7,7 @@ import core.api.models.timemanager.taskSchedule.scheduleInfos.AdvancedScheduleIn import core.api.models.timemanager.taskSchedule.scheduleInfos.BasicScheduleFieldInfo; import core.api.models.timemanager.taskSchedule.ForgottenScheduleInfo; import core.api.models.timemanager.taskSchedule.scheduleInfos.ScheduleInfo; -import core.entities.timemanager.AbstractSchedule; -import core.entities.timemanager.AdvancedTaskSchedule; -import core.entities.timemanager.BasicTaskSchedule; -import core.entities.timemanager.Task; +import core.entities.timemanager.*; import core.repositories.UserRepository; import core.repositories.timemanager.AdvancedScheduleRepository; import core.repositories.timemanager.ScheduleRepository; @@ -259,4 +256,8 @@ public class TaskScheduleService { schedule.setStopTime(schedule.getStartTime().plusMinutes(manualScheduleStopInfo.getDuration())); scheduleRepository.save(schedule); } + + public void deleteSchedulesByTaskgroup(Taskgroup taskgroup) { + scheduleRepository.deleteByTaskgroup(taskgroup); + } } diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index 2074798..168cdda 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -113,6 +113,7 @@ public class TaskService { public void clearTasks(Taskgroup taskgroup) { taskSeriesService.deleteTaskSerieByTaskgroup(taskgroup); + taskScheduleService.deleteSchedulesByTaskgroup(taskgroup); taskRepository.deleteAllByTaskgroup(taskgroup); } diff --git a/backend/src/main/java/core/services/TaskgroupService.java b/backend/src/main/java/core/services/TaskgroupService.java index ebbc078..b0a2408 100644 --- a/backend/src/main/java/core/services/TaskgroupService.java +++ b/backend/src/main/java/core/services/TaskgroupService.java @@ -21,10 +21,12 @@ public class TaskgroupService { private final TaskgroupRepository taskgroupRepository; private final UserRepository userRepository; + private final TaskService taskService; public TaskgroupService(@Autowired TaskgroupRepository taskgroupRepository, - @Autowired UserRepository userRepository) { + @Autowired UserRepository userRepository, @Autowired TaskService taskService) { this.taskgroupRepository = taskgroupRepository; this.userRepository = userRepository; + this.taskService = taskService; } public PermissionResult getTaskgroupByIDAndUsername(long taskgroupID, String username) { @@ -97,6 +99,7 @@ public class TaskgroupService { } public void deleteTaskgroup(Taskgroup taskgroup) { + taskService.clearTasks(taskgroup); taskgroupRepository.delete(taskgroup); } From f0d50a280e09eee431872cebd1a1258cc3b8b858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 07:30:49 +0100 Subject: [PATCH 10/18] Adapt weekly repeating algorithm to consider subtasks --- .../repeatinginfo/TaskRepeatWeekDayInfo.java | 9 ++ .../java/core/entities/timemanager/Task.java | 75 +++++++++-- .../core/entities/timemanager/TaskSerie.java | 5 + .../entities/timemanager/TaskSerieItem.java | 6 + .../timemanager/TaskRepository.java | 9 ++ .../java/core/services/TaskSeriesService.java | 117 ++++++++++-------- .../main/java/core/services/TaskService.java | 1 + backend/src/main/java/util/Tripel.java | 15 +++ backend/src/main/java/util/Tupel.java | 20 +++ 9 files changed, 192 insertions(+), 65 deletions(-) create mode 100644 backend/src/main/java/util/Tripel.java create mode 100644 backend/src/main/java/util/Tupel.java diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java index 4330b2b..ab43ff6 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java @@ -8,6 +8,7 @@ import java.time.DayOfWeek; public class TaskRepeatWeekDayInfo { private int offset; private long taskID; + private DayOfWeek dayOfWeek; public int getOffset() { return offset; @@ -24,4 +25,12 @@ public class TaskRepeatWeekDayInfo { public void setTaskID(long taskID) { this.taskID = taskID; } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } } diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index c1a8d0f..8d05038 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -1,14 +1,15 @@ package core.entities.timemanager; import core.api.models.timemanager.tasks.TaskFieldInfo; +import core.api.models.timemanager.tasks.repeatinginfo.DeadlineStrategy; +import util.Tripel; +import util.Tupel; import javax.persistence.*; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.time.temporal.ChronoUnit; +import java.util.*; @Entity @Table(name = "tasks") @@ -54,14 +55,59 @@ public class Task { this.finishable = taskFieldInfo.isFinishable(); } - public static Task cloneTask(Task task) { + public Tripel, Collection> cloneTask() { + Collection clonedTasks = new ArrayList<>(); + Collection clonedSchedules = new ArrayList<>(); + Task clonedTask = new Task(); - clonedTask.setTaskgroup(task.getTaskgroup()); - clonedTask.setTaskName(task.taskName); - clonedTask.setEta(task.eta); - clonedTask.setFinished(false); - clonedTask.setFinishable(task.finishable); - return clonedTask; + clonedTasks.add(clonedTask); + + clonedTask.setTaskgroup(this.getTaskgroup()); + clonedTask.setTaskName(this.taskName); + clonedTask.setEta(this.eta); + clonedTask.setFinished(this.finished); + clonedTask.setFinishable(this.finishable); + clonedTask.setStartDate(this.startDate); + clonedTask.setDeadline(this.deadline); + + for(AbstractSchedule abstractSchedule : this.basicTaskSchedules) { + AbstractSchedule clonedSchedule = abstractSchedule.cloneSchedule(); + clonedSchedules.add(clonedSchedule); + } + + Set clonedSubtasks = new HashSet<>(); + for(Task task : this.subtasks) { + Tripel, Collection> clonedSubtask = task.cloneTask(); + clonedSubtask.getValue00().setParent(clonedTask); + clonedSubtasks.add(clonedSubtask.getValue00()); + + clonedTasks.addAll(clonedSubtask.getValue01()); + clonedSchedules.addAll(clonedSubtask.getValue02()); + } + clonedTask.setSubtasks(clonedSubtasks); + return new Tripel<>(clonedTask, clonedTasks, clonedSchedules); + } + + public void shiftTask(long startingDayDifference, long endingDayDifference) { + this.setStartDate(this.getStartDate().plusDays(startingDayDifference)); + this.setDeadline(this.getDeadline().plusDays(endingDayDifference)); + + for(AbstractSchedule abstractSchedule : this.basicTaskSchedules) { + abstractSchedule.shiftSchedule(startingDayDifference); + } + + for(Task subtask: this.subtasks) { + subtask.shiftTask(startingDayDifference, endingDayDifference); + } + } + + public void shiftTask(long startingDayDifference) { + this.setStartDate(this.getStartDate().plusDays(startingDayDifference)); + this.setDeadline(this.getStartDate()); + + for(Task subtask: this.subtasks) { + subtask.shiftTask(startingDayDifference); + } } @@ -253,4 +299,11 @@ public class Task { public void increaseWorkTime(long minutes) { this.workTime += (int) minutes; } + + public void shiftStartingTask(long dayDifference) { + this.setStartDate(this.getStartDate().plusDays(dayDifference)); + for (Task subtask : this.subtasks) { + subtask.shiftTask(dayDifference); + } + } } diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerie.java b/backend/src/main/java/core/entities/timemanager/TaskSerie.java index 50be1c7..87df8cd 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerie.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerie.java @@ -39,4 +39,9 @@ public class TaskSerie { this.tasks.add(taskSerieItem); return taskSerieItem; } + + public void addItem(TaskSerieItem taskSerieItem) { + this.tasks.add(taskSerieItem); + taskSerieItem.setTaskSerie(this); + } } diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java index 533a931..3cf7ee8 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java @@ -31,6 +31,12 @@ public class TaskSerieItem { public TaskSerieItem() { } + public TaskSerieItem(Task task, int itemIndex) { + this.taskSerie = null; + this.seriesIndex = itemIndex; + this.task = task; + } + public long getItemID() { return itemID; } diff --git a/backend/src/main/java/core/repositories/timemanager/TaskRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskRepository.java index 208dbe6..df0bdda 100644 --- a/backend/src/main/java/core/repositories/timemanager/TaskRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/TaskRepository.java @@ -25,6 +25,7 @@ public interface TaskRepository extends CrudRepository { @Transactional @Modifying + @Query(value = "DELETE FROM Task t WHERE t.taskID = ?1") void deleteByTaskID(long taskID); @Query(value = "SELECT t FROM Task t WHERE t.taskgroup.user.username = ?1 AND t.deadline is NOT NULL AND t.deadline < ?2 AND t.finished = FALSE") @@ -35,4 +36,12 @@ public interface TaskRepository extends CrudRepository { @Query(value = "SELECT t FROM Task t WHERE t.taskgroup.user.username = ?1 AND (t.startDate IS NULL OR t.startDate <= ?2) AND t.finished = FALSE") List findAllActive(String username, LocalDate now); + + @Query(value = "SELECT t FROM Task t WHERE t.taskgroup = ?1") + List findAllByTaskgroup(Taskgroup taskgroup); + + @Modifying + @Transactional + @Query(value = "UPDATE Task t SET t.parent = null WHERE t.taskgroup = ?1") + void deleteTaskHierarchyWhereTaskgroup(Taskgroup taskgroup); } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 7f1eb85..93827fd 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -11,14 +11,13 @@ import core.repositories.timemanager.TaskSerieItemRepository; import core.repositories.timemanager.TaskSeriesRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import util.Tupel; +import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalDate; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; +import java.util.*; @Service public class TaskSeriesService { @@ -30,59 +29,79 @@ public class TaskSeriesService { public ServiceExitCode createTaskSeries(TaskRepeatWeekInfo taskRepeatInfo) { - List createdTasks = new ArrayList<>(); + Map> offsetMap = calcOffsetMap(taskRepeatInfo); + TaskSerie taskSerie = new TaskSerie(); - List abstractSchedules = new ArrayList<>(); + List clonedTasks = new ArrayList<>(); + List clonedSchedules = new ArrayList<>(); + int weekDayIndex = 0; + for(TaskRepeatWeekDayInfo weekDayInfo : taskRepeatInfo.getWeekDayInfos()) { + Optional requestedTask = taskRepository.findById(weekDayInfo.getTaskID()); + if(requestedTask.isEmpty()) return ServiceExitCode.MISSING_ENTITY; + Task rootTask = requestedTask.get(); - for(TaskRepeatWeekDayInfo taskRepeatDayInfo : taskRepeatInfo.getWeekDayInfos()) { - Optional task = taskRepository.findById(taskRepeatDayInfo.getTaskID()); - if(task.isEmpty()) return ServiceExitCode.MISSING_ENTITY; + LocalDate currentDate = rootTask.getStartDate().plusDays(weekDayInfo.getOffset()); - TaskSerieItem rootItem = taskSerie.addTask(task.get()); - task.get().setTaskSerieItem(rootItem); + int itemIndex = weekDayIndex; + while(currentDate.isBefore(taskRepeatInfo.getEndDate())) { + var cloneResult = rootTask.cloneTask(); + Task clonedRootTask = cloneResult.getValue00(); + clonedTasks.addAll(cloneResult.getValue01()); + clonedSchedules.addAll(cloneResult.getValue02()); - LocalDate currentTaskDate = task.get().getStartDate().plusDays(taskRepeatDayInfo.getOffset()); - while(currentTaskDate.isBefore(taskRepeatInfo.getEndDate())) { - Task clonedTask = Task.cloneTask(task.get()); - clonedTask.setStartDate(currentTaskDate); - - TaskSerieItem taskSerieItem = taskSerie.addTask(clonedTask); - clonedTask.setTaskSerieItem(taskSerieItem); - createdTasks.add(clonedTask); - - abstractSchedules.addAll(cloneSchedules(task.get(), clonedTask)); - currentTaskDate = currentTaskDate.plusDays(taskRepeatDayInfo.getOffset()); - } - } - - taskSerie.getTasks().sort(Comparator.comparing(o -> o.getTask().getStartDate())); - for(int i=0; i offsetEntry = offsetMap.get(weekDayInfo); + clonedRootTask.shiftTask(offsetEntry.getValue00(), offsetEntry.getValue01()); + + currentDate = currentDate.plusDays(weekDayInfo.getOffset()); + itemIndex += taskRepeatInfo.getWeekDayInfos().size(); } + + + weekDayIndex++; } + + taskSeriesRepository.save(taskSerie); - taskRepository.saveAll(createdTasks); + taskRepository.saveAll(clonedTasks); taskSerieItemRepository.saveAll(taskSerie.getTasks()); - scheduleRepository.saveAll(abstractSchedules); + scheduleRepository.saveAll(clonedSchedules); return ServiceExitCode.OK; } + private HashMap> calcOffsetMap(TaskRepeatWeekInfo weekInfo) { + HashMap> offsetMap = new HashMap<>(); + weekInfo.getWeekDayInfos().sort(Comparator.comparing(TaskRepeatWeekDayInfo::getDayOfWeek)); + for(int i=0; i(offset, offset)); + } else { + int startingOffset = weekInfo.getWeekDayInfos().get(i).getOffset() -1; + + DayOfWeek nextWeekday = null; + if(i == weekInfo.getWeekDayInfos().size()-1) { + nextWeekday = weekInfo.getWeekDayInfos().get(0).getDayOfWeek(); + } else { + nextWeekday = weekInfo.getWeekDayInfos().get(i+1).getDayOfWeek(); + } + + DayOfWeek currentWeekDay = weekInfo.getWeekDayInfos().get(i).getDayOfWeek(); + + int endingOffset = startingOffset + Math.abs(nextWeekday.getValue() - currentWeekDay.getValue()); + + offsetMap.put(weekInfo.getWeekDayInfos().get(i), new Tupel<>(startingOffset, endingOffset)); + } + } + return offsetMap; + } + public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatDayInfo taskRepeatInfo) { if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.FIX_DEADLINE) { return ServiceExitCode.INVALID_PARAMETER; @@ -96,18 +115,8 @@ public class TaskSeriesService { LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { - Task task = Task.cloneTask(rootTask); - task.setStartDate(currentTaskDate); - if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) { - task.setDeadline(currentTaskDate); - } else if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_FIT_START) { - task.setDeadline(currentTaskDate.plusDays(taskRepeatInfo.getOffset()-1)); - } - TaskSerieItem taskSerieItem = taskSerie.addTask(task); - taskList.add(task); - task.setTaskSerieItem(taskSerieItem); + Tupel> clonedTasks = rootTask.cloneTask(); - abstractSchedules.addAll(cloneSchedules(rootTask, task)); currentTaskDate = currentTaskDate.plusDays(taskRepeatInfo.getOffset()); } diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index 168cdda..ae89548 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -114,6 +114,7 @@ public class TaskService { public void clearTasks(Taskgroup taskgroup) { taskSeriesService.deleteTaskSerieByTaskgroup(taskgroup); taskScheduleService.deleteSchedulesByTaskgroup(taskgroup); + taskRepository.deleteTaskHierarchyWhereTaskgroup(taskgroup); taskRepository.deleteAllByTaskgroup(taskgroup); } diff --git a/backend/src/main/java/util/Tripel.java b/backend/src/main/java/util/Tripel.java new file mode 100644 index 0000000..71ab146 --- /dev/null +++ b/backend/src/main/java/util/Tripel.java @@ -0,0 +1,15 @@ +package util; + +public class Tripel extends Tupel { + + private final C value02; + + public Tripel(A value00, B value01, C value02) { + super(value00, value01); + this.value02 = value02; + } + + public C getValue02() { + return value02; + } +} diff --git a/backend/src/main/java/util/Tupel.java b/backend/src/main/java/util/Tupel.java new file mode 100644 index 0000000..b7749e7 --- /dev/null +++ b/backend/src/main/java/util/Tupel.java @@ -0,0 +1,20 @@ +package util; + +public class Tupel { + + private final A value00; + private final B value01; + + public Tupel(A value00, B value01) { + this.value00 = value00; + this.value01 = value01; + } + + public A getValue00() { + return value00; + } + + public B getValue01() { + return value01; + } +} From fea17c3fb9f9ea991eb040d108f7555cf0a89f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 07:48:06 +0100 Subject: [PATCH 11/18] Refactor Code for Repeating Weekly --- .../repeatinginfo/TaskRepeatWeekInfo.java | 4 + .../java/core/services/TaskSeriesService.java | 110 ++++++++++-------- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java index 39b4b22..eadf689 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java @@ -2,9 +2,13 @@ package core.api.models.timemanager.tasks.repeatinginfo; import com.fasterxml.jackson.annotation.JsonFormat; import org.hibernate.validator.constraints.Length; +import util.Tupel; import javax.validation.constraints.Size; +import java.time.DayOfWeek; import java.time.LocalDate; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; public class TaskRepeatWeekInfo { diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 93827fd..10c752f 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -29,44 +29,27 @@ public class TaskSeriesService { public ServiceExitCode createTaskSeries(TaskRepeatWeekInfo taskRepeatInfo) { - Map> offsetMap = calcOffsetMap(taskRepeatInfo); + HashMap> offsetMap = calcWeeklyOffsetMap(taskRepeatInfo); TaskSerie taskSerie = new TaskSerie(); List clonedTasks = new ArrayList<>(); List clonedSchedules = new ArrayList<>(); int weekDayIndex = 0; - for(TaskRepeatWeekDayInfo weekDayInfo : taskRepeatInfo.getWeekDayInfos()) { - Optional requestedTask = taskRepository.findById(weekDayInfo.getTaskID()); - if(requestedTask.isEmpty()) return ServiceExitCode.MISSING_ENTITY; - Task rootTask = requestedTask.get(); + for(Map.Entry> repeatingTaskInfo: offsetMap.entrySet()) { + Task rootTask = repeatingTaskInfo.getKey(); + TaskSerieItem rootItem = new TaskSerieItem(rootTask, weekDayIndex); + taskSerie.addItem(rootItem); - LocalDate currentDate = rootTask.getStartDate().plusDays(weekDayInfo.getOffset()); - - int itemIndex = weekDayIndex; - while(currentDate.isBefore(taskRepeatInfo.getEndDate())) { - var cloneResult = rootTask.cloneTask(); - Task clonedRootTask = cloneResult.getValue00(); - clonedTasks.addAll(cloneResult.getValue01()); - clonedSchedules.addAll(cloneResult.getValue02()); - - for(Task clonedTask : cloneResult.getValue01()) { - TaskSerieItem item = new TaskSerieItem(clonedTask, itemIndex); - taskSerie.addItem(item); - } - - Tupel offsetEntry = offsetMap.get(weekDayInfo); - clonedRootTask.shiftTask(offsetEntry.getValue00(), offsetEntry.getValue01()); - - currentDate = currentDate.plusDays(weekDayInfo.getOffset()); - itemIndex += taskRepeatInfo.getWeekDayInfos().size(); - } + int itemIndex = weekDayIndex +1; + Tupel, Collection> repeatingResult = repeatTask(rootTask, taskRepeatInfo.getEndDate(), offsetMap, taskSerie, itemIndex); + clonedTasks.addAll(repeatingResult.getValue00()); + clonedSchedules.addAll(repeatingResult.getValue01()); weekDayIndex++; } - taskSeriesRepository.save(taskSerie); taskRepository.saveAll(clonedTasks); taskSerieItemRepository.saveAll(taskSerie.getTasks()); @@ -75,33 +58,69 @@ public class TaskSeriesService { return ServiceExitCode.OK; } - private HashMap> calcOffsetMap(TaskRepeatWeekInfo weekInfo) { - HashMap> offsetMap = new HashMap<>(); + private Tupel, Collection> repeatTask(Task rootTask, LocalDate endingDate, HashMap> offsetMap,TaskSerie taskSerie, int itemIndex) { + List clonedTasks = new ArrayList<>(); + List clonedSchedules = new ArrayList<>(); + + LocalDate currentDate = rootTask.getStartDate().plusDays(offsetMap.get(rootTask).getValue00()); + while(currentDate.isBefore(endingDate)) { + var cloneResult = rootTask.cloneTask(); + Task clonedRootTask = cloneResult.getValue00(); + clonedTasks.addAll(cloneResult.getValue01()); + clonedSchedules.addAll(cloneResult.getValue02()); + + for(Task clonedTask : cloneResult.getValue01()) { + TaskSerieItem item = new TaskSerieItem(clonedTask, itemIndex); + taskSerie.addItem(item); + } + + Tupel offsetEntry = offsetMap.get(rootTask); + clonedRootTask.shiftTask(offsetEntry.getValue00(), offsetEntry.getValue01()); + + currentDate = currentDate.plusDays(offsetEntry.getValue00()); + itemIndex += offsetMap.size(); + } + + return new Tupel<>(clonedTasks, clonedSchedules); + } + + private HashMap> calcWeeklyOffsetMap(TaskRepeatWeekInfo weekInfo) throws NoSuchElementException { + HashMap> offsetMap = new HashMap<>(); weekInfo.getWeekDayInfos().sort(Comparator.comparing(TaskRepeatWeekDayInfo::getDayOfWeek)); for(int i=0; i(offset, offset)); + Optional requestedTask = taskRepository.findById(weekInfo.getWeekDayInfos().get(i).getTaskID()); + if(requestedTask.isEmpty()) { + throw new NoSuchElementException(); } else { - int startingOffset = weekInfo.getWeekDayInfos().get(i).getOffset() -1; - - DayOfWeek nextWeekday = null; - if(i == weekInfo.getWeekDayInfos().size()-1) { - nextWeekday = weekInfo.getWeekDayInfos().get(0).getDayOfWeek(); + if(weekInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) { + int offset = weekInfo.getWeekDayInfos().get(i).getOffset()-1; + offsetMap.put(requestedTask.get(), new Tupel<>(offset, offset)); } else { - nextWeekday = weekInfo.getWeekDayInfos().get(i+1).getDayOfWeek(); + int startingOffset = weekInfo.getWeekDayInfos().get(i).getOffset() -1; + int endingOffset = getEndingOffset(weekInfo, i, startingOffset); + + offsetMap.put(requestedTask.get(), new Tupel<>(startingOffset, endingOffset)); } - - DayOfWeek currentWeekDay = weekInfo.getWeekDayInfos().get(i).getDayOfWeek(); - - int endingOffset = startingOffset + Math.abs(nextWeekday.getValue() - currentWeekDay.getValue()); - - offsetMap.put(weekInfo.getWeekDayInfos().get(i), new Tupel<>(startingOffset, endingOffset)); } + } return offsetMap; } + private static int getEndingOffset(TaskRepeatWeekInfo weekInfo, int i, int startingOffset) { + DayOfWeek nextWeekday; + if(i == weekInfo.getWeekDayInfos().size()-1) { + nextWeekday = weekInfo.getWeekDayInfos().get(0).getDayOfWeek(); + } else { + nextWeekday = weekInfo.getWeekDayInfos().get(i +1).getDayOfWeek(); + } + + DayOfWeek currentWeekDay = weekInfo.getWeekDayInfos().get(i).getDayOfWeek(); + + int endingOffset = startingOffset + Math.abs(nextWeekday.getValue() - currentWeekDay.getValue()); + return endingOffset; + } + public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatDayInfo taskRepeatInfo) { if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.FIX_DEADLINE) { return ServiceExitCode.INVALID_PARAMETER; @@ -113,12 +132,7 @@ public class TaskSeriesService { TaskSerieItem rootItem = taskSerie.addTask(rootTask); rootTask.setTaskSerieItem(rootItem); - LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); - while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { - Tupel> clonedTasks = rootTask.cloneTask(); - currentTaskDate = currentTaskDate.plusDays(taskRepeatInfo.getOffset()); - } taskSeriesRepository.save(taskSerie); taskRepository.saveAll(taskList); From 9b995baa75bc00f0acc10ee20813c944ad1beb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 08:01:48 +0100 Subject: [PATCH 12/18] Remove DeadlineStrategy as it is not longer necessary --- .../repeatinginfo/TaskRepeatDayInfo.java | 9 --- .../repeatinginfo/TaskRepeatWeekInfo.java | 10 --- .../java/core/entities/timemanager/Task.java | 26 ++----- .../java/core/services/TaskSeriesService.java | 71 +++++-------------- 4 files changed, 23 insertions(+), 93 deletions(-) diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java index a188057..8bcf9b1 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java @@ -7,7 +7,6 @@ import java.time.LocalDate; public class TaskRepeatDayInfo { private int offset; - private DeadlineStrategy deadlineStrategy; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") private LocalDate endingDate; @@ -20,14 +19,6 @@ public class TaskRepeatDayInfo { this.offset = offset; } - public DeadlineStrategy getDeadlineStrategy() { - return deadlineStrategy; - } - - public void setDeadlineStrategy(DeadlineStrategy deadlineStrategy) { - this.deadlineStrategy = deadlineStrategy; - } - public LocalDate getEndingDate() { return endingDate; } diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java index eadf689..4838296 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java @@ -15,7 +15,6 @@ public class TaskRepeatWeekInfo { @Size(min = 1, max = 7) private List weekDayInfos; - private DeadlineStrategy deadlineStrategy; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") private LocalDate endDate; @@ -26,15 +25,6 @@ public class TaskRepeatWeekInfo { public void setWeekDayInfos(List weekDayInfos) { this.weekDayInfos = weekDayInfos; } - - public DeadlineStrategy getDeadlineStrategy() { - return deadlineStrategy; - } - - public void setDeadlineStrategy(DeadlineStrategy deadlineStrategy) { - this.deadlineStrategy = deadlineStrategy; - } - public LocalDate getEndDate() { return endDate; } diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index 8d05038..efe9fd8 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -88,28 +88,19 @@ public class Task { return new Tripel<>(clonedTask, clonedTasks, clonedSchedules); } - public void shiftTask(long startingDayDifference, long endingDayDifference) { - this.setStartDate(this.getStartDate().plusDays(startingDayDifference)); - this.setDeadline(this.getDeadline().plusDays(endingDayDifference)); + public void shiftTask(long offset) { + this.setStartDate(this.getStartDate().plusDays(offset)); + this.setDeadline(this.getDeadline().plusDays(offset)); for(AbstractSchedule abstractSchedule : this.basicTaskSchedules) { - abstractSchedule.shiftSchedule(startingDayDifference); + abstractSchedule.shiftSchedule(offset); } for(Task subtask: this.subtasks) { - subtask.shiftTask(startingDayDifference, endingDayDifference); + subtask.shiftTask(offset); } } - public void shiftTask(long startingDayDifference) { - this.setStartDate(this.getStartDate().plusDays(startingDayDifference)); - this.setDeadline(this.getStartDate()); - - for(Task subtask: this.subtasks) { - subtask.shiftTask(startingDayDifference); - } - } - public long getTaskID() { return taskID; @@ -299,11 +290,4 @@ public class Task { public void increaseWorkTime(long minutes) { this.workTime += (int) minutes; } - - public void shiftStartingTask(long dayDifference) { - this.setStartDate(this.getStartDate().plusDays(dayDifference)); - for (Task subtask : this.subtasks) { - subtask.shiftTask(dayDifference); - } - } } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 10c752f..11764e3 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -29,13 +29,13 @@ public class TaskSeriesService { public ServiceExitCode createTaskSeries(TaskRepeatWeekInfo taskRepeatInfo) { - HashMap> offsetMap = calcWeeklyOffsetMap(taskRepeatInfo); + HashMap offsetMap = calcWeeklyOffsetMap(taskRepeatInfo); TaskSerie taskSerie = new TaskSerie(); List clonedTasks = new ArrayList<>(); List clonedSchedules = new ArrayList<>(); int weekDayIndex = 0; - for(Map.Entry> repeatingTaskInfo: offsetMap.entrySet()) { + for(Map.Entry repeatingTaskInfo: offsetMap.entrySet()) { Task rootTask = repeatingTaskInfo.getKey(); TaskSerieItem rootItem = new TaskSerieItem(rootTask, weekDayIndex); taskSerie.addItem(rootItem); @@ -58,11 +58,11 @@ public class TaskSeriesService { return ServiceExitCode.OK; } - private Tupel, Collection> repeatTask(Task rootTask, LocalDate endingDate, HashMap> offsetMap,TaskSerie taskSerie, int itemIndex) { + private Tupel, Collection> repeatTask(Task rootTask, LocalDate endingDate, HashMap offsetMap,TaskSerie taskSerie, int itemIndex) { List clonedTasks = new ArrayList<>(); List clonedSchedules = new ArrayList<>(); - LocalDate currentDate = rootTask.getStartDate().plusDays(offsetMap.get(rootTask).getValue00()); + LocalDate currentDate = rootTask.getStartDate().plusDays(offsetMap.get(rootTask)); while(currentDate.isBefore(endingDate)) { var cloneResult = rootTask.cloneTask(); Task clonedRootTask = cloneResult.getValue00(); @@ -74,87 +74,52 @@ public class TaskSeriesService { taskSerie.addItem(item); } - Tupel offsetEntry = offsetMap.get(rootTask); - clonedRootTask.shiftTask(offsetEntry.getValue00(), offsetEntry.getValue01()); + clonedRootTask.shiftTask(offsetMap.get(rootTask)); - currentDate = currentDate.plusDays(offsetEntry.getValue00()); + currentDate = currentDate.plusDays(offsetMap.get(rootTask)); itemIndex += offsetMap.size(); } return new Tupel<>(clonedTasks, clonedSchedules); } - private HashMap> calcWeeklyOffsetMap(TaskRepeatWeekInfo weekInfo) throws NoSuchElementException { - HashMap> offsetMap = new HashMap<>(); + private HashMap calcWeeklyOffsetMap(TaskRepeatWeekInfo weekInfo) throws NoSuchElementException { + HashMap offsetMap = new HashMap<>(); weekInfo.getWeekDayInfos().sort(Comparator.comparing(TaskRepeatWeekDayInfo::getDayOfWeek)); for(int i=0; i requestedTask = taskRepository.findById(weekInfo.getWeekDayInfos().get(i).getTaskID()); if(requestedTask.isEmpty()) { throw new NoSuchElementException(); } else { - if(weekInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) { - int offset = weekInfo.getWeekDayInfos().get(i).getOffset()-1; - offsetMap.put(requestedTask.get(), new Tupel<>(offset, offset)); - } else { - int startingOffset = weekInfo.getWeekDayInfos().get(i).getOffset() -1; - int endingOffset = getEndingOffset(weekInfo, i, startingOffset); - - offsetMap.put(requestedTask.get(), new Tupel<>(startingOffset, endingOffset)); - } + int offset = weekInfo.getWeekDayInfos().get(i).getOffset()-1; + offsetMap.put(requestedTask.get(), offset); } } return offsetMap; } - private static int getEndingOffset(TaskRepeatWeekInfo weekInfo, int i, int startingOffset) { - DayOfWeek nextWeekday; - if(i == weekInfo.getWeekDayInfos().size()-1) { - nextWeekday = weekInfo.getWeekDayInfos().get(0).getDayOfWeek(); - } else { - nextWeekday = weekInfo.getWeekDayInfos().get(i +1).getDayOfWeek(); - } - - DayOfWeek currentWeekDay = weekInfo.getWeekDayInfos().get(i).getDayOfWeek(); - - int endingOffset = startingOffset + Math.abs(nextWeekday.getValue() - currentWeekDay.getValue()); - return endingOffset; - } - public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatDayInfo taskRepeatInfo) { - if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.FIX_DEADLINE) { - return ServiceExitCode.INVALID_PARAMETER; - } - - List taskList = new ArrayList<>(); - List abstractSchedules = new ArrayList<>(); TaskSerie taskSerie = new TaskSerie(); TaskSerieItem rootItem = taskSerie.addTask(rootTask); rootTask.setTaskSerieItem(rootItem); + HashMap offsetMap = new HashMap<>(); + offsetMap.put(rootTask, taskRepeatInfo.getOffset()); + + var repeatingResult = repeatTask(rootTask, taskRepeatInfo.getEndingDate(), offsetMap, taskSerie, 0); + List clonedTasks = new ArrayList<>(repeatingResult.getValue00()); + List clonedSchedules = new ArrayList<>(repeatingResult.getValue01()); taskSeriesRepository.save(taskSerie); - taskRepository.saveAll(taskList); + taskRepository.saveAll(clonedTasks); taskSerieItemRepository.saveAll(taskSerie.getTasks()); - scheduleRepository.saveAll(abstractSchedules); + scheduleRepository.saveAll(clonedSchedules); return ServiceExitCode.OK; } - public List cloneSchedules(Task previousTask, Task nextTask) { - long numberDays = ChronoUnit.DAYS.between(previousTask.getStartDate(), nextTask.getStartDate()); - - List clonedSchedules = new ArrayList<>(); - for(AbstractSchedule abstractSchedule : previousTask.getBasicTaskSchedules()) { - AbstractSchedule clonedSchedule = abstractSchedule.cloneSchedule(); - clonedSchedule.shiftSchedule(numberDays); - - clonedSchedules.add(clonedSchedule); - } - return clonedSchedules; - } - public void deleteTaskSeriesItem(Task task) { TaskSerieItem item = task.getTaskSerieItem(); TaskSerie taskSerie = task.getTaskSerieItem().getTaskSerie(); From f080c6db52909d8906e4b04cd04c811c477a617b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 08:21:56 +0100 Subject: [PATCH 13/18] Adjust Frontend to removed DeadlineStrategy --- frontend/src/api/model/taskRepeatDayInfo.ts | 9 ------- .../src/api/model/taskRepeatWeekDayInfo.ts | 17 +++++++++++++ frontend/src/api/model/taskRepeatWeekInfo.ts | 9 ------- .../task-series-creator.component.html | 7 ------ .../task-series-creator.component.ts | 12 ---------- .../task-weekly-series-creator.component.html | 8 ------- .../task-weekly-series-creator.component.ts | 23 ++++++++++++++---- openapi.yaml | 24 +++++++++---------- 8 files changed, 47 insertions(+), 62 deletions(-) diff --git a/frontend/src/api/model/taskRepeatDayInfo.ts b/frontend/src/api/model/taskRepeatDayInfo.ts index 31b219e..e91f598 100644 --- a/frontend/src/api/model/taskRepeatDayInfo.ts +++ b/frontend/src/api/model/taskRepeatDayInfo.ts @@ -16,18 +16,9 @@ export interface TaskRepeatDayInfo { * number repeating days */ offset: number; - deadlineStrategy: TaskRepeatDayInfo.DeadlineStrategyEnum; /** * Date until the tasks repeat */ endingDate: string; } -export namespace TaskRepeatDayInfo { - export type DeadlineStrategyEnum = 'DEADLINE_EQUAL_START' | 'DEADLINE_FIT_START'; - export const DeadlineStrategyEnum = { - EqualStart: 'DEADLINE_EQUAL_START' as DeadlineStrategyEnum, - FitStart: 'DEADLINE_FIT_START' as DeadlineStrategyEnum - }; -} - diff --git a/frontend/src/api/model/taskRepeatWeekDayInfo.ts b/frontend/src/api/model/taskRepeatWeekDayInfo.ts index ed3390e..87b61f3 100644 --- a/frontend/src/api/model/taskRepeatWeekDayInfo.ts +++ b/frontend/src/api/model/taskRepeatWeekDayInfo.ts @@ -20,5 +20,22 @@ export interface TaskRepeatWeekDayInfo { * internal identifier of task */ taskID: number; + /** + * day of week + */ + dayOfWeek: TaskRepeatWeekDayInfo.DayOfWeekEnum; +} +export namespace TaskRepeatWeekDayInfo { + export type DayOfWeekEnum = 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY' | 'SUNDAY'; + export const DayOfWeekEnum = { + Monday: 'MONDAY' as DayOfWeekEnum, + Tuesday: 'TUESDAY' as DayOfWeekEnum, + Wednesday: 'WEDNESDAY' as DayOfWeekEnum, + Thursday: 'THURSDAY' as DayOfWeekEnum, + Friday: 'FRIDAY' as DayOfWeekEnum, + Saturday: 'SATURDAY' as DayOfWeekEnum, + Sunday: 'SUNDAY' as DayOfWeekEnum + }; } + diff --git a/frontend/src/api/model/taskRepeatWeekInfo.ts b/frontend/src/api/model/taskRepeatWeekInfo.ts index 6803c5c..e0e0b7a 100644 --- a/frontend/src/api/model/taskRepeatWeekInfo.ts +++ b/frontend/src/api/model/taskRepeatWeekInfo.ts @@ -13,19 +13,10 @@ import { TaskRepeatWeekDayInfo } from './taskRepeatWeekDayInfo'; export interface TaskRepeatWeekInfo { - deadlineStrategy: TaskRepeatWeekInfo.DeadlineStrategyEnum; /** * Date until the tasks repeat */ endDate: string; weekDayInfos: Array; } -export namespace TaskRepeatWeekInfo { - export type DeadlineStrategyEnum = 'DEADLINE_EQUAL_START' | 'DEADLINE_FIT_START'; - export const DeadlineStrategyEnum = { - EqualStart: 'DEADLINE_EQUAL_START' as DeadlineStrategyEnum, - FitStart: 'DEADLINE_FIT_START' as DeadlineStrategyEnum - }; -} - diff --git a/frontend/src/app/tasks/task-series-creator/task-series-creator.component.html b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.html index e000da5..59ab72a 100644 --- a/frontend/src/app/tasks/task-series-creator/task-series-creator.component.html +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.html @@ -7,13 +7,6 @@ Offset - - Deadline Strategy - - Fit Next Start - Equal Same Start - -
diff --git a/frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts index ed27ba5..36d5da1 100644 --- a/frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts @@ -2,7 +2,6 @@ import {Component, Inject} from '@angular/core'; import {FormBuilder, Validators} from "@angular/forms"; import {TaskEntityInfo, TaskRepeatDayInfo, TaskseriesService} from "../../../api"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; -import DeadlineStrategyEnum = TaskRepeatDayInfo.DeadlineStrategyEnum; import * as moment from "moment"; @@ -15,7 +14,6 @@ export class TaskSeriesCreatorComponent { dailyFormGroup = this._formBuilder.group({ offsetCtrl: ['', Validators.required], - deadlineStrategyCtrl: ['', Validators.required] }) endDateFormGroup = this._formBuilder.group({ @@ -32,7 +30,6 @@ export class TaskSeriesCreatorComponent { save() { this.taskSeriesService.tasksTaskIDTaskseriesDailyPost(this.task.taskID,{ offset: Number(this.dailyFormGroup.get('offsetCtrl')!.value!), - deadlineStrategy: this.convertDeadlineStrategyCtrlToDeadlineEnum(), endingDate: moment( this.endDateFormGroup.get('endDateCtrl')!.value!).format('YYYY-MM-DDTHH:mm:ss.SSSZ') }).subscribe({ next: resp => { @@ -40,13 +37,4 @@ export class TaskSeriesCreatorComponent { } }) } - - convertDeadlineStrategyCtrlToDeadlineEnum() { - const deadlineStrategy = this.dailyFormGroup.get('deadlineStrategyCtrl')!.value; - if(deadlineStrategy === DeadlineStrategyEnum.EqualStart) { - return DeadlineStrategyEnum.EqualStart; - } else { - return DeadlineStrategyEnum.FitStart; - } - } } diff --git a/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.html b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.html index 2d18e9b..e4ebefb 100644 --- a/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.html +++ b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.html @@ -24,14 +24,6 @@ - - Deadline-Strategy - - - {{deadlineStrategy}} - - - Ending Date diff --git a/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.ts b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.ts index 9b40131..f062284 100644 --- a/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.ts +++ b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.ts @@ -3,7 +3,7 @@ import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {TaskEntityInfo, TaskRepeatDayInfo, TaskRepeatWeekDayInfo, TaskseriesService} from "../../../api"; import * as moment from "moment"; import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms"; -import DeadlineStrategyEnum = TaskRepeatDayInfo.DeadlineStrategyEnum; +import DayOfWeekEnum = TaskRepeatWeekDayInfo.DayOfWeekEnum; @Component({ @@ -14,7 +14,6 @@ import DeadlineStrategyEnum = TaskRepeatDayInfo.DeadlineStrategyEnum; export class TaskWeeklySeriesCreatorComponent implements OnInit{ repeatingOffsetForm: FormGroup | undefined - availableDeadlineStrategys: DeadlineStrategyEnum[] = ["DEADLINE_EQUAL_START", "DEADLINE_FIT_START"] constructor(private dialogRef: MatDialogRef, private taskRepeatingService: TaskseriesService, @@ -27,7 +26,6 @@ export class TaskWeeklySeriesCreatorComponent implements OnInit{ offsets: this.formbuilder.array(this.tasks.map(task => this.formbuilder.group({ offsetCtrl: ['', [Validators.required, Validators.min(1)]] }))), - deadlineStrategyCtrl: ['', Validators.required], endingDateCtrl: ['', Validators.required] }) @@ -62,12 +60,12 @@ export class TaskWeeklySeriesCreatorComponent implements OnInit{ for(let i=0; i Date: Sun, 17 Mar 2024 08:28:02 +0100 Subject: [PATCH 14/18] Fix unshifted Schedules --- backend/src/main/java/core/entities/timemanager/Task.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index efe9fd8..52d38cd 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -73,6 +73,8 @@ public class Task { for(AbstractSchedule abstractSchedule : this.basicTaskSchedules) { AbstractSchedule clonedSchedule = abstractSchedule.cloneSchedule(); clonedSchedules.add(clonedSchedule); + clonedTask.getBasicTaskSchedules().clear(); + clonedTask.getBasicTaskSchedules().add(clonedSchedule); } Set clonedSubtasks = new HashSet<>(); From 7336daddb1753b4bfa762bf6d3c67f24db066947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 08:32:33 +0100 Subject: [PATCH 15/18] Add root(sub)tasks to taskserie --- .../java/core/services/TaskSeriesService.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 11764e3..6e555c4 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -37,8 +37,7 @@ public class TaskSeriesService { int weekDayIndex = 0; for(Map.Entry repeatingTaskInfo: offsetMap.entrySet()) { Task rootTask = repeatingTaskInfo.getKey(); - TaskSerieItem rootItem = new TaskSerieItem(rootTask, weekDayIndex); - taskSerie.addItem(rootItem); + addRootSubTasksToTaskSerie(taskSerie, rootTask, weekDayIndex); int itemIndex = weekDayIndex +1; Tupel, Collection> repeatingResult = repeatTask(rootTask, taskRepeatInfo.getEndDate(), offsetMap, taskSerie, itemIndex); @@ -101,8 +100,7 @@ public class TaskSeriesService { public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatDayInfo taskRepeatInfo) { TaskSerie taskSerie = new TaskSerie(); - TaskSerieItem rootItem = taskSerie.addTask(rootTask); - rootTask.setTaskSerieItem(rootItem); + addRootSubTasksToTaskSerie(taskSerie, rootTask, 0); HashMap offsetMap = new HashMap<>(); offsetMap.put(rootTask, taskRepeatInfo.getOffset()); @@ -120,6 +118,16 @@ public class TaskSeriesService { return ServiceExitCode.OK; } + private void addRootSubTasksToTaskSerie(TaskSerie taskSerie, Task rootTask, int index) { + Queue taskQueue = new LinkedList<>(Collections.singletonList(rootTask)); + while(!taskQueue.isEmpty()) { + Task currentTask = taskQueue.poll(); + + TaskSerieItem taskSerieItem = new TaskSerieItem(currentTask, index); + taskSerie.addItem(taskSerieItem); + } + } + public void deleteTaskSeriesItem(Task task) { TaskSerieItem item = task.getTaskSerieItem(); TaskSerie taskSerie = task.getTaskSerieItem().getTaskSerie(); From 56d75fead9d075b0ed86d156c3d8a755d1b51bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 08:42:58 +0100 Subject: [PATCH 16/18] Fix Repair Indexing to consider subtasks --- .../main/java/core/services/TaskSeriesService.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 6e555c4..1eb3bcc 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -141,21 +141,19 @@ public class TaskSeriesService { taskSerie.getTasks().clear(); taskSeriesRepository.delete(taskSerie); } else { - repearIndexing(taskSerie); + repearIndexing(taskSerie, item.getSeriesIndex()); } } - private void repearIndexing(TaskSerie taskSerie) { + private void repearIndexing(TaskSerie taskSerie, int deletedIndex) { taskSerie.getTasks().sort(Comparator.comparingInt(TaskSerieItem::getSeriesIndex)); List updatedItems = new ArrayList<>(); - int currentIndex = 1; + for(TaskSerieItem taskSerieItem : taskSerie.getTasks()) { - if(taskSerieItem.getSeriesIndex() != currentIndex) { - taskSerieItem.setSeriesIndex(currentIndex); + if(taskSerieItem.getSeriesIndex() > deletedIndex) { + taskSerieItem.setSeriesIndex(taskSerieItem.getSeriesIndex() -1); updatedItems.add(taskSerieItem); } - - currentIndex++; } taskSerieItemRepository.saveAll(updatedItems); From 4059500fa4a0c0ef709e049e5925831bbedc4b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 09:16:20 +0100 Subject: [PATCH 17/18] Clear Subtasks and fix indexing only when non subtask is deleted --- .../core/api/controller/TaskController.java | 11 +++++++ .../timemanager/TaskRepository.java | 5 +++ .../java/core/services/TaskSeriesService.java | 2 +- .../main/java/core/services/TaskService.java | 5 +++ openapi.yaml | 33 ++++++++++++++++++- 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/core/api/controller/TaskController.java b/backend/src/main/java/core/api/controller/TaskController.java index c40c660..c51b8a1 100644 --- a/backend/src/main/java/core/api/controller/TaskController.java +++ b/backend/src/main/java/core/api/controller/TaskController.java @@ -196,4 +196,15 @@ public class TaskController { Collection subtasks = taskPermissionResult.getResult().getSubtasks(); return ResponseEntity.ok(subtasks.stream().map(TaskEntityInfo::new).toList()); } + + @DeleteMapping("/tasks/{taskID}/subtasks") + public ResponseEntity onClearSubtasks(@PathVariable long taskID) { + var taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(taskPermissionResult.hasIssue()) { + return taskPermissionResult.mapToResponseEntity(); + } + + ServiceExitCode result = taskService.clearSubTasks(taskPermissionResult.getResult()); + return result.mapToResponseEntity(); + } } diff --git a/backend/src/main/java/core/repositories/timemanager/TaskRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskRepository.java index df0bdda..e5a7dd5 100644 --- a/backend/src/main/java/core/repositories/timemanager/TaskRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/TaskRepository.java @@ -44,4 +44,9 @@ public interface TaskRepository extends CrudRepository { @Transactional @Query(value = "UPDATE Task t SET t.parent = null WHERE t.taskgroup = ?1") void deleteTaskHierarchyWhereTaskgroup(Taskgroup taskgroup); + + @Modifying + @Transactional + @Query(value = "DELETE Task t WHERE t.parent = ?1") + void deleteTasksByParent(Task parentTask); } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 1eb3bcc..4d99d09 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -140,7 +140,7 @@ public class TaskSeriesService { } taskSerie.getTasks().clear(); taskSeriesRepository.delete(taskSerie); - } else { + } else if(task.getParent() == null){ repearIndexing(taskSerie, item.getSeriesIndex()); } } diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index ae89548..8ac2e33 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -159,4 +159,9 @@ public class TaskService { } } + + public ServiceExitCode clearSubTasks(Task parentTask) { + taskRepository.deleteTasksByParent(parentTask); + return ServiceExitCode.OK; + } } diff --git a/openapi.yaml b/openapi.yaml index 953e5d9..4df888a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1349,7 +1349,38 @@ paths: application/json: schema: $ref: '#/components/schemas/SimpleStatusResponse' - + delete: + security: + - API_TOKEN: [] + tags: + - task + parameters: + - name: taskID + in: path + description: internal id of task + required: true + schema: + type: number + example: 1 + responses: + 200: + description: Operation successfull + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 403: + description: No permission + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' + 404: + description: Task not found + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' /schedules: From 0a60b8b27e3dfd9ce3a71f6730fafed3c2a52813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sun, 17 Mar 2024 09:16:37 +0100 Subject: [PATCH 18/18] Clear subtasks in UI --- frontend/src/api/api/task.service.ts | 57 +++++++++++++++++ .../clear-task-dialog.component.ts | 62 +++++++++++++------ .../task-dashboard.component.ts | 9 ++- .../task-detail-overview.component.html | 4 +- 4 files changed, 108 insertions(+), 24 deletions(-) diff --git a/frontend/src/api/api/task.service.ts b/frontend/src/api/api/task.service.ts index c15e918..6299a9a 100644 --- a/frontend/src/api/api/task.service.ts +++ b/frontend/src/api/api/task.service.ts @@ -401,6 +401,63 @@ export class TaskService { ); } + /** + * @param taskID internal id of task + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public tasksTaskIDSubtasksDelete(taskID: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskIDSubtasksDelete(taskID: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDSubtasksDelete(taskID: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDSubtasksDelete(taskID: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + if (taskID === null || taskID === undefined) { + throw new Error('Required parameter taskID was null or undefined when calling tasksTaskIDSubtasksDelete.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (API_TOKEN) required + localVarCredential = this.configuration.lookupCredential('API_TOKEN'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Bearer ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + + let responseType_: 'text' | 'json' = 'json'; + if(localVarHttpHeaderAcceptSelected && localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } + + return this.httpClient.delete(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/subtasks`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + /** * @param taskID internal id of task * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. diff --git a/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts index 86c8af5..400dd26 100644 --- a/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts +++ b/frontend/src/app/tasks/clear-task-dialog/clear-task-dialog.component.ts @@ -1,12 +1,14 @@ import {Component, Inject, OnInit} from '@angular/core'; import {TaskEditorData} from "../task-editor/TaskEditorData"; -import {TaskEntityInfo, TaskgroupService} from "../../../api"; +import {TaskEntityInfo, TaskgroupService, TaskService} from "../../../api"; import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; import {MatSnackBar} from "@angular/material/snack-bar"; export interface ClearTaskDialogData { - taskgroupID: number, - tasks: TaskEntityInfo[] + taskgroupID: number | undefined, + parentTaskID: number | undefined + tasks: TaskEntityInfo[], + subtasks: boolean } @Component({ selector: 'app-clear-task-dialog', @@ -23,7 +25,8 @@ export class ClearTaskDialogComponent implements OnInit{ constructor(@Inject(MAT_DIALOG_DATA) public editorData: ClearTaskDialogData, private taskgroupService: TaskgroupService, private dialogRef: MatDialogRef, - private snackbar: MatSnackBar) { + private snackbar: MatSnackBar, + private taskService: TaskService) { } ngOnInit(): void { @@ -48,22 +51,43 @@ export class ClearTaskDialogComponent implements OnInit{ } clearTasks() { - this.taskgroupService.taskgroupsTaskgroupIDClearDelete(this.editorData.taskgroupID).subscribe({ - next: resp => { - if(resp.status == "success") { - const deletedTasks = this.editorData.tasks; - this.dialogRef.close(deletedTasks); + if(this.editorData.subtasks) { + this.taskService.tasksTaskIDSubtasksDelete(this.editorData.parentTaskID!).subscribe({ + next: resp => { + if(resp.status == "success") { + const deletedTasks = this.editorData.tasks; + this.dialogRef.close(deletedTasks); + } + }, + error: err => { + if(err.status == 403) { + this.snackbar.open("No permission", "", {duration: 2000}); + } else if(err.status == 404) { + this.snackbar.open("Not found", "", {duration: 2000}); + } else { + this.snackbar.open("Unexpected error", "", {duration: 2000}); + } } - }, - error: err => { - if(err.status == 403) { - this.snackbar.open("No permission", "", {duration: 2000}); - } else if(err.status == 404) { - this.snackbar.open("Not found", "", {duration: 2000}); - } else { - this.snackbar.open("Unexpected error", "", {duration: 2000}); + }) + } else { + this.taskgroupService.taskgroupsTaskgroupIDClearDelete(this.editorData.taskgroupID!).subscribe({ + next: resp => { + if(resp.status == "success") { + const deletedTasks = this.editorData.tasks; + this.dialogRef.close(deletedTasks); + } + }, + error: err => { + if(err.status == 403) { + this.snackbar.open("No permission", "", {duration: 2000}); + } else if(err.status == 404) { + this.snackbar.open("Not found", "", {duration: 2000}); + } else { + this.snackbar.open("Unexpected error", "", {duration: 2000}); + } } - } - }) + }) + } + } } diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts index a961599..18277d3 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -20,7 +20,7 @@ import {TaskWeeklySeriesCreatorComponent} from "../task-weekly-series-creator/ta }) export class TaskDashboardComponent implements OnChanges{ ngOnChanges(): void { - if(this.taskgroupID != undefined) { + if(this.subTasks.length == 0 && this.taskgroupID != undefined) { this.fetchTasks() } else if(this.subTasks.length > 0) { this.datasource.data = this.subTasks; @@ -33,6 +33,7 @@ export class TaskDashboardComponent implements OnChanges{ @Input("taskgroupID") taskgroupID: number | undefined @Input("subTasks") subTasks: TaskEntityInfo[] = [] + @Input("parentTaskID") parentTaskID: number | undefined @ViewChild(MatPaginator) paginator: MatPaginator | undefined @ViewChild(MatSort) sort: MatSort | undefined @@ -111,9 +112,11 @@ export class TaskDashboardComponent implements OnChanges{ } clearTasks() { - const clearTaskData: ClearTaskDialogData = { + let clearTaskData: ClearTaskDialogData = { tasks: this.datasource.data, - taskgroupID: this.taskgroupID! + taskgroupID: this.taskgroupID, + parentTaskID: this.parentTaskID, + subtasks: this.subTasks.length > 0 } const dialogRef = this.dialog.open(ClearTaskDialogComponent, {data: clearTaskData, width: "600px"}); dialogRef.afterClosed().subscribe(res => { diff --git a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html index c530aad..6ab6c82 100644 --- a/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html +++ b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html @@ -44,8 +44,8 @@ Subtasks -
- +
+