From 3f58fe3600fb38383c6b32e239a8df98ad791f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 08:57:54 +0100 Subject: [PATCH 01/17] Implement Backend Components for Creating Daily Taskseries --- .../api/controller/ScheduleController.java | 22 +++---- .../api/controller/StatisticController.java | 6 +- .../core/api/controller/TaskController.java | 12 ++-- .../api/controller/TaskSeriesController.java | 45 +++++++++++++ .../api/controller/TaskgroupController.java | 9 ++- .../tasks/repeatinginfo/DeadlineStrategy.java | 8 +++ .../repeatinginfo/TaskRepeatDayInfo.java | 35 ++++++++++ .../repeatinginfo/TaskRepeatWeekDayInfo.java | 9 +++ .../repeatinginfo/TaskRepeatWeekInfo.java | 40 ++++++++++++ .../java/core/entities/timemanager/Task.java | 29 +++++++-- .../core/entities/timemanager/TaskSerie.java | 36 +++++++++++ .../timemanager/TaskSeriesRepository.java | 9 +++ .../java/core/services/PermissionResult.java | 21 +++++- .../java/core/services/ServiceExitCode.java | 12 ++++ .../java/core/services/ServiceResult.java | 11 ++++ .../java/core/services/TaskSeriesService.java | 59 +++++++++++++++++ openapi.yaml | 64 ++++++++++++++++++- 17 files changed, 390 insertions(+), 37 deletions(-) create mode 100644 backend/src/main/java/core/api/controller/TaskSeriesController.java create mode 100644 backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/DeadlineStrategy.java create mode 100644 backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java create mode 100644 backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java create mode 100644 backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java create mode 100644 backend/src/main/java/core/entities/timemanager/TaskSerie.java create mode 100644 backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java create mode 100644 backend/src/main/java/core/services/TaskSeriesService.java diff --git a/backend/src/main/java/core/api/controller/ScheduleController.java b/backend/src/main/java/core/api/controller/ScheduleController.java index 5242b9d..d07a91c 100644 --- a/backend/src/main/java/core/api/controller/ScheduleController.java +++ b/backend/src/main/java/core/api/controller/ScheduleController.java @@ -41,7 +41,7 @@ public class ScheduleController { @GetMapping("/schedules/{taskID}") public ResponseEntity loadAllSchedulesOfTask(@PathVariable long taskID) { PermissionResult permissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -72,7 +72,7 @@ public class ScheduleController { private ResponseEntity createAbstractSchedule(long taskID, ScheduleFieldInfo scheduleFieldInfo) { PermissionResult permissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -111,7 +111,7 @@ public class ScheduleController { private ResponseEntity editAbstractSchedule(long scheduleID, ScheduleFieldInfo scheduleFieldInfo) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -141,7 +141,7 @@ public class ScheduleController { @DeleteMapping("/schedules/{scheduleID}") public ResponseEntity deleteSchedule(@PathVariable long scheduleID) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -163,7 +163,7 @@ public class ScheduleController { @PostMapping("/schedules/{taskID}/now") public ResponseEntity scheduleNow(@PathVariable long taskID) { PermissionResult permissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -193,7 +193,7 @@ public class ScheduleController { @PostMapping("/schedules/{scheduleID}/activate") public ResponseEntity activateSchedule(@PathVariable long scheduleID) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -217,7 +217,7 @@ public class ScheduleController { @PostMapping("/schedules/{scheduleID}/stop/{finish}") public ResponseEntity stopSchedule(@PathVariable long scheduleID, @PathVariable boolean finish) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -232,7 +232,7 @@ public class ScheduleController { @PostMapping("/schedules/{taskID}/forgotten") public ResponseEntity registerForgottenSchedule(@PathVariable long taskID, @RequestBody @Valid ForgottenScheduleInfo forgottenScheduleInfo) { PermissionResult permissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -259,7 +259,7 @@ public class ScheduleController { List> permissionResults = new ArrayList<>(); for(long scheduleID: scheduleIDs) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -278,7 +278,7 @@ public class ScheduleController { @GetMapping("/schedules/{scheduleID}/details") public ResponseEntity loadScheduleDetails(@PathVariable long scheduleID) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -292,7 +292,7 @@ public class ScheduleController { @PostMapping("/schedules/{scheduleID}/stopManual") public ResponseEntity stopManual(@PathVariable long scheduleID, @Valid @RequestBody ManualScheduleStopInfo manualScheduleStopInfo) { PermissionResult permissionResult = taskScheduleService.getSchedulePermissions(scheduleID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!permissionResult.isHasPermissions()) { + if(permissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } diff --git a/backend/src/main/java/core/api/controller/StatisticController.java b/backend/src/main/java/core/api/controller/StatisticController.java index af632be..36e2e0e 100644 --- a/backend/src/main/java/core/api/controller/StatisticController.java +++ b/backend/src/main/java/core/api/controller/StatisticController.java @@ -3,9 +3,7 @@ package core.api.controller; import core.api.models.auth.SimpleStatusResponse; import core.api.models.timemanager.history.TaskgroupActivityInfo; import core.api.models.timemanager.history.WorkingStatus; -import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo; import core.entities.timemanager.AbstractSchedule; -import core.entities.timemanager.Task; import core.entities.timemanager.Taskgroup; import core.services.PermissionResult; import core.services.ServiceExitCode; @@ -14,12 +12,10 @@ import core.services.TaskgroupService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.parameters.P; import org.springframework.web.bind.annotation.*; import java.time.*; import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAdjusters; import java.util.*; @CrossOrigin(origins = "*", maxAge = 3600) @@ -49,7 +45,7 @@ public class StatisticController { @GetMapping("/statistics/taskgroup-activity/{taskgroupID}/{startingDate}/{endingDate}/{includeSubTaskgroups}") public ResponseEntity getTaskgroupActivity(@PathVariable long taskgroupID, @PathVariable String startingDate, @PathVariable String endingDate, @PathVariable boolean includeSubTaskgroups){ PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!taskgroupPermissionResult.isHasPermissions()) { + if(taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } else if(taskgroupPermissionResult.getExitCode() == ServiceExitCode.MISSING_ENTITY) { return ResponseEntity.status(404).body(new SimpleStatusResponse("failed")); diff --git a/backend/src/main/java/core/api/controller/TaskController.java b/backend/src/main/java/core/api/controller/TaskController.java index 0d814f0..823e1aa 100644 --- a/backend/src/main/java/core/api/controller/TaskController.java +++ b/backend/src/main/java/core/api/controller/TaskController.java @@ -64,7 +64,7 @@ public class TaskController { @GetMapping("/tasks/{taskgroupID}/{status}") public ResponseEntity listTasksOfTaskgroup(@PathVariable long taskgroupID, @PathVariable String status) { PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!taskgroupPermissionResult.isHasPermissions()) { + if(taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -78,7 +78,7 @@ public class TaskController { @PutMapping("/tasks/{taskgroupID}") public ResponseEntity createTask(@PathVariable long taskgroupID, @RequestBody @Valid TaskFieldInfo taskFieldInfo) { PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!taskgroupPermissionResult.isHasPermissions()) { + if(taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -101,7 +101,7 @@ public class TaskController { @PostMapping("/tasks/{taskID}") public ResponseEntity editTask(@PathVariable long taskID, @RequestBody @Valid TaskFieldInfo taskFieldInfo) { PermissionResult taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if (!taskPermissionResult.isHasPermissions()) { + if (taskPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -124,7 +124,7 @@ public class TaskController { @DeleteMapping("/tasks/{taskID}") public ResponseEntity deleteTask(@PathVariable long taskID) { PermissionResult taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if (!taskPermissionResult.isHasPermissions()) { + if (taskPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -139,7 +139,7 @@ public class TaskController { @GetMapping("/tasks/{taskID}") public ResponseEntity loadTaskDetails(@PathVariable long taskID) { PermissionResult taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if (!taskPermissionResult.isHasPermissions()) { + if (taskPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -153,7 +153,7 @@ public class TaskController { @PostMapping("/tasks/{taskID}/finish") public ResponseEntity finishTask(@PathVariable long taskID) { PermissionResult taskPermissionResult = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if (!taskPermissionResult.isHasPermissions()) { + if (taskPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } diff --git a/backend/src/main/java/core/api/controller/TaskSeriesController.java b/backend/src/main/java/core/api/controller/TaskSeriesController.java new file mode 100644 index 0000000..c2d2994 --- /dev/null +++ b/backend/src/main/java/core/api/controller/TaskSeriesController.java @@ -0,0 +1,45 @@ +package core.api.controller; + +import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatDayInfo; +import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekInfo; +import core.services.ServiceExitCode; +import core.services.ServiceResult; +import core.services.TaskSeriesService; +import core.services.TaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("/api") +public class TaskSeriesController { + + @Autowired private TaskService taskService; + @Autowired private TaskSeriesService taskSeriesService; + + @PostMapping("/tasks/{taskID}/taskseries/weekly") + public ResponseEntity onCreateTaskSeries(@PathVariable long taskID, @Valid @RequestBody TaskRepeatWeekInfo taskRepeatWeekInfo) { + var taskPermission = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(taskPermission.hasIssue()) { + return taskPermission.mapToResponseEntity(); + } else { + ServiceExitCode serviceExitCode = taskSeriesService.createTaskSeries(taskPermission.getResult(), taskRepeatWeekInfo); + return serviceExitCode.mapToResponseEntity(); + } + } + + @PostMapping("/tasks/{taskID}/taskseries/daily") + public ResponseEntity onCreateTaskSeries(@PathVariable long taskID, @Valid @RequestBody TaskRepeatDayInfo taskRepeatDayInfo) { + var taskPermission = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); + if(taskPermission.hasIssue()) { + return taskPermission.mapToResponseEntity(); + } else { + ServiceExitCode serviceExitCode = taskSeriesService.createTaskSeries(taskPermission.getResult(), taskRepeatDayInfo); + return serviceExitCode.mapToResponseEntity(); + } + } +} diff --git a/backend/src/main/java/core/api/controller/TaskgroupController.java b/backend/src/main/java/core/api/controller/TaskgroupController.java index e9cdea2..7183f13 100644 --- a/backend/src/main/java/core/api/controller/TaskgroupController.java +++ b/backend/src/main/java/core/api/controller/TaskgroupController.java @@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; -import java.util.Set; @CrossOrigin(origins = "*", maxAge = 3600) @RestController @@ -40,7 +39,7 @@ public class TaskgroupController { @PostMapping("/taskgroups/{taskgroupID}") public ResponseEntity editTaskgroup(@PathVariable long taskgroupID, @Valid @RequestBody TaskgroupFieldInfo taskgroupFieldInfo) { PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!taskgroupPermissionResult.isHasPermissions()) { + if(taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -59,7 +58,7 @@ public class TaskgroupController { @DeleteMapping("/taskgroups/{taskgroupID}") public ResponseEntity deleteTaskgroup(@PathVariable long taskgroupID) { PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if (!taskgroupPermissionResult.isHasPermissions()) { + if (taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -88,7 +87,7 @@ public class TaskgroupController { @GetMapping("/taskgroups/{taskgroupID}") public ResponseEntity getDetails(@PathVariable long taskgroupID) { PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(!taskgroupPermissionResult.isHasPermissions()) { + if(taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } @@ -102,7 +101,7 @@ public class TaskgroupController { @DeleteMapping("/taskgroups/{taskgroupID}/clear") public ResponseEntity clearTasks(@PathVariable long taskgroupID) { PermissionResult taskgroupPermissionResult = taskgroupService.getTaskgroupByIDAndUsername(taskgroupID, SecurityContextHolder.getContext().getAuthentication().getName()); - if (!taskgroupPermissionResult.isHasPermissions()) { + if (taskgroupPermissionResult.isNoPermissions()) { return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); } diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/DeadlineStrategy.java b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/DeadlineStrategy.java new file mode 100644 index 0000000..13d93c0 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/DeadlineStrategy.java @@ -0,0 +1,8 @@ +package core.api.models.timemanager.tasks.repeatinginfo; + +public enum DeadlineStrategy { + + FIX_DEADLINE, + DEADLINE_EQUAL_START, + DEADLINE_FIT_START +} 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 new file mode 100644 index 0000000..65d91c9 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java @@ -0,0 +1,35 @@ +package core.api.models.timemanager.tasks.repeatinginfo; + +import java.time.LocalDate; + +public class TaskRepeatDayInfo { + + private int offset; + private DeadlineStrategy deadlineStrategy; + + private LocalDate endDate; + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public DeadlineStrategy getDeadlineStrategy() { + return deadlineStrategy; + } + + public void setDeadlineStrategy(DeadlineStrategy deadlineStrategy) { + this.deadlineStrategy = deadlineStrategy; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } +} 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 new file mode 100644 index 0000000..8589f14 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java @@ -0,0 +1,9 @@ +package core.api.models.timemanager.tasks.repeatinginfo; + +public class TaskRepeatWeekDayInfo { + + private int weekday; + private int offset; + + +} 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 new file mode 100644 index 0000000..19a4ce8 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java @@ -0,0 +1,40 @@ +package core.api.models.timemanager.tasks.repeatinginfo; + +import org.hibernate.validator.constraints.Length; + +import java.time.LocalDate; +import java.util.List; + +public class TaskRepeatWeekInfo { + + @Length(min = 1, max = 7) + private List weekDayInfos; + + private DeadlineStrategy deadlineStrategy; + + private LocalDate endDate; + + public List getWeekDayInfos() { + return weekDayInfos; + } + + 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; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } +} diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index b6bf550..728101b 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -22,21 +22,17 @@ public class Task { @JoinColumn(name = "taskgroup_id") private Taskgroup taskgroup; private String taskName; - private LocalDate startDate; - private LocalDate deadline; - private int eta; - private boolean finished; - private boolean finishable; - @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) private List basicTaskSchedules; - private int workTime; + @ManyToOne + @JoinColumn(referencedColumnName = "taskSerieID") + private TaskSerie taskSerie; public Task() { this.basicTaskSchedules = new ArrayList<>(); @@ -52,6 +48,17 @@ public class Task { this.finishable = taskFieldInfo.isFinishable(); } + public static Task cloneTask(Task task) { + 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; + } + + public long getTaskID() { return taskID; } @@ -124,6 +131,14 @@ public class Task { this.taskID = taskID; } + public TaskSerie getTaskSerie() { + return taskSerie; + } + + public void setTaskSerie(TaskSerie taskSerie) { + this.taskSerie = taskSerie; + } + public List getBasicTaskSchedules() { if(basicTaskSchedules == null) { return new ArrayList<>(); diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerie.java b/backend/src/main/java/core/entities/timemanager/TaskSerie.java new file mode 100644 index 0000000..f317f34 --- /dev/null +++ b/backend/src/main/java/core/entities/timemanager/TaskSerie.java @@ -0,0 +1,36 @@ +package core.entities.timemanager; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "task_series") +public class TaskSerie { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long taskSerieID; + + @OneToMany(orphanRemoval = false, cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "taskSerie") + List tasks = new ArrayList<>(); + + + + + public long getTaskSerieID() { + return taskSerieID; + } + + public List getTasks() { + return tasks; + } + + public void setTasks(List tasks) { + this.tasks = tasks; + } + + public void addTask(Task task) { + this.tasks.add(task); + } +} diff --git a/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java new file mode 100644 index 0000000..d03fc42 --- /dev/null +++ b/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java @@ -0,0 +1,9 @@ +package core.repositories.timemanager; + +import core.entities.timemanager.TaskSerie; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TaskSeriesRepository extends CrudRepository { +} diff --git a/backend/src/main/java/core/services/PermissionResult.java b/backend/src/main/java/core/services/PermissionResult.java index aafe4ef..c7b9a76 100644 --- a/backend/src/main/java/core/services/PermissionResult.java +++ b/backend/src/main/java/core/services/PermissionResult.java @@ -1,5 +1,8 @@ package core.services; +import core.api.models.auth.SimpleStatusResponse; +import org.springframework.http.ResponseEntity; + public class PermissionResult extends ServiceResult { private boolean hasPermissions; @@ -30,11 +33,25 @@ public class PermissionResult extends ServiceResult { this.hasPermissions = hasPermissions; } - public boolean isHasPermissions() { - return hasPermissions; + public boolean isNoPermissions() { + return !hasPermissions; } public void setHasPermissions(boolean hasPermissions) { this.hasPermissions = hasPermissions; } + + @Override + public ResponseEntity mapToResponseEntity() { + if(isNoPermissions()) { + return ResponseEntity.status(403).body(new SimpleStatusResponse("failed")); + } else { + return super.mapToResponseEntity(); + } + } + + @Override + public boolean hasIssue() { + return super.hasIssue() || isNoPermissions(); + } } diff --git a/backend/src/main/java/core/services/ServiceExitCode.java b/backend/src/main/java/core/services/ServiceExitCode.java index 93cbddd..452f52c 100644 --- a/backend/src/main/java/core/services/ServiceExitCode.java +++ b/backend/src/main/java/core/services/ServiceExitCode.java @@ -1,5 +1,8 @@ package core.services; +import core.api.models.auth.SimpleStatusResponse; +import org.springframework.http.ResponseEntity; + public enum ServiceExitCode { OK, @@ -7,4 +10,13 @@ public enum ServiceExitCode { MISSING_ENTITY, INVALID_OPERATION, INVALID_PARAMETER; + + public ResponseEntity mapToResponseEntity() { + return switch (this) { + case OK -> ResponseEntity.ok(new SimpleStatusResponse("success")); + case MISSING_ENTITY -> ResponseEntity.status(404).body(new SimpleStatusResponse("failed")); + case ENTITY_ALREADY_EXIST -> ResponseEntity.status(409).body(new SimpleStatusResponse("failed")); + case INVALID_OPERATION, INVALID_PARAMETER -> ResponseEntity.status(400).body(new SimpleStatusResponse("failed")); + }; + } } diff --git a/backend/src/main/java/core/services/ServiceResult.java b/backend/src/main/java/core/services/ServiceResult.java index da9e03d..179b514 100644 --- a/backend/src/main/java/core/services/ServiceResult.java +++ b/backend/src/main/java/core/services/ServiceResult.java @@ -1,5 +1,8 @@ package core.services; +import core.api.models.auth.SimpleStatusResponse; +import org.springframework.http.ResponseEntity; + public class ServiceResult { private ServiceExitCode exitCode; @@ -34,4 +37,12 @@ public class ServiceResult { public void setResult(T result) { this.result = result; } + + public boolean hasIssue() { + return exitCode != ServiceExitCode.OK; + } + + public ResponseEntity mapToResponseEntity() { + return exitCode.mapToResponseEntity(); + } } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java new file mode 100644 index 0000000..278ecf2 --- /dev/null +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -0,0 +1,59 @@ +package core.services; + +import core.api.models.timemanager.tasks.repeatinginfo.DeadlineStrategy; +import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatDayInfo; +import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekInfo; +import core.entities.timemanager.Task; +import core.entities.timemanager.TaskSerie; +import core.repositories.timemanager.TaskRepository; +import core.repositories.timemanager.TaskSeriesRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; + +@Service +public class TaskSeriesService { + + @Autowired private TaskRepository taskRepository; + @Autowired private TaskSeriesRepository taskSeriesRepository; + + public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatWeekInfo taskRepeatInfo) { + return ServiceExitCode.OK; + } + + public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatDayInfo taskRepeatInfo) { + if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.FIX_DEADLINE) { + return ServiceExitCode.INVALID_PARAMETER; + } + + LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); + TaskSerie taskSerie = new TaskSerie(); + int index = 1; + while(currentTaskDate.isBefore(taskRepeatInfo.getEndDate())) { + 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)); + } + taskSerie.addTask(task); + task.setTaskSerie(taskSerie); + task.setTaskName(task.getTaskName().replace("${i}", convertIndexToString(index))); + currentTaskDate = currentTaskDate.plusDays(taskRepeatInfo.getOffset()); + index++; + } + + taskRepository.saveAll(taskSerie.getTasks()); + return ServiceExitCode.OK; + } + + public static String convertIndexToString(int index) { + if(index < 10) { + return "0" + index; + } else { + return String.valueOf(index); + } + } +} diff --git a/openapi.yaml b/openapi.yaml index 2001027..b83f363 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2073,6 +2073,52 @@ paths: application/json: schema: $ref: '#/components/schemas/SimpleStatusResponse' + /tasks/{taskID}/taskseries/daily: + post: + security: + - API_TOKEN: [] + tags: + - taskseries + description: Creates a daily repeating task + summary: daily repeating task creation + parameters: + - name: taskID + in: path + description: internal id of taskgroup + required: true + schema: + type: number + example: 1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TaskRepeatDayInfo' + 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' + 400: + description: Invalid deadlineStrategy + content: + application/json: + schema: + $ref: '#/components/schemas/SimpleStatusResponse' components: @@ -2779,4 +2825,20 @@ components: example: password ntfy_token: type: string - description: token to ntfy useraccount \ No newline at end of file + description: token to ntfy useraccount + TaskRepeatDayInfo: + required: + - offset + - deadlineStrategy + additionalProperties: false + properties: + offset: + type: number + description: number repeating days + example: 7 + minimum: 1 + deadlineStrategy: + type: string + enum: + - DEADLINE_EQUAL_START + - DEADLINE_FIT_START \ No newline at end of file From a3723e3459bedce0a55c7798666f641b6b28758a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 09:43:23 +0100 Subject: [PATCH 02/17] Frontend Components for Creating Daily Repeating Tasks --- .../tasks/repeatinginfo/TaskRepeatDayInfo.java | 13 ++++++++----- .../main/java/core/services/TaskSeriesService.java | 3 ++- frontend/src/api/.openapi-generator/FILES | 2 ++ frontend/src/api/api.module.ts | 1 + frontend/src/api/api/api.ts | 4 +++- frontend/src/api/model/models.ts | 1 + frontend/src/api/model/ntfyInformation.ts | 2 +- frontend/src/app/app.module.ts | 4 ++++ .../task-detail-overview.component.html | 2 +- .../task-detail-overview.component.ts | 8 ++++++++ .../ntfy-settings/ntfy-settings.component.ts | 2 +- 11 files changed, 32 insertions(+), 10 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 65d91c9..a188057 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 @@ -1,5 +1,7 @@ package core.api.models.timemanager.tasks.repeatinginfo; +import com.fasterxml.jackson.annotation.JsonFormat; + import java.time.LocalDate; public class TaskRepeatDayInfo { @@ -7,7 +9,8 @@ public class TaskRepeatDayInfo { private int offset; private DeadlineStrategy deadlineStrategy; - private LocalDate endDate; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private LocalDate endingDate; public int getOffset() { return offset; @@ -25,11 +28,11 @@ public class TaskRepeatDayInfo { this.deadlineStrategy = deadlineStrategy; } - public LocalDate getEndDate() { - return endDate; + public LocalDate getEndingDate() { + return endingDate; } - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; + public void setEndingDate(LocalDate endingDate) { + this.endingDate = endingDate; } } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 278ecf2..4c6ec67 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -30,7 +30,7 @@ public class TaskSeriesService { LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); TaskSerie taskSerie = new TaskSerie(); int index = 1; - while(currentTaskDate.isBefore(taskRepeatInfo.getEndDate())) { + while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { Task task = Task.cloneTask(rootTask); task.setStartDate(currentTaskDate); if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) { @@ -45,6 +45,7 @@ public class TaskSeriesService { index++; } + taskSeriesRepository.save(taskSerie); taskRepository.saveAll(taskSerie.getTasks()); return ServiceExitCode.OK; } diff --git a/frontend/src/api/.openapi-generator/FILES b/frontend/src/api/.openapi-generator/FILES index 3595c4b..3469f4b 100644 --- a/frontend/src/api/.openapi-generator/FILES +++ b/frontend/src/api/.openapi-generator/FILES @@ -10,6 +10,7 @@ api/properties.service.ts api/schedule.service.ts api/task.service.ts api/taskgroup.service.ts +api/taskseries.service.ts api/users.service.ts configuration.ts encoder.ts @@ -47,6 +48,7 @@ model/simpleStatusResponse.ts model/taskEntityInfo.ts model/taskFieldInfo.ts model/taskOverviewInfo.ts +model/taskRepeatDayInfo.ts model/taskScheduleStopResponse.ts model/taskShortInfo.ts model/taskTaskgroupInfo.ts diff --git a/frontend/src/api/api.module.ts b/frontend/src/api/api.module.ts index 63562e7..f2f84b6 100644 --- a/frontend/src/api/api.module.ts +++ b/frontend/src/api/api.module.ts @@ -10,6 +10,7 @@ import { PropertiesService } from './api/properties.service'; import { ScheduleService } from './api/schedule.service'; import { TaskService } from './api/task.service'; import { TaskgroupService } from './api/taskgroup.service'; +import { TaskseriesService } from './api/taskseries.service'; import { UsersService } from './api/users.service'; @NgModule({ diff --git a/frontend/src/api/api/api.ts b/frontend/src/api/api/api.ts index 75dbaae..37fe840 100644 --- a/frontend/src/api/api/api.ts +++ b/frontend/src/api/api/api.ts @@ -14,6 +14,8 @@ export * from './task.service'; import { TaskService } from './task.service'; export * from './taskgroup.service'; import { TaskgroupService } from './taskgroup.service'; +export * from './taskseries.service'; +import { TaskseriesService } from './taskseries.service'; export * from './users.service'; import { UsersService } from './users.service'; -export const APIS = [AccountService, HistoryService, LoginService, NtfyService, PropertiesService, ScheduleService, TaskService, TaskgroupService, UsersService]; +export const APIS = [AccountService, HistoryService, LoginService, NtfyService, PropertiesService, ScheduleService, TaskService, TaskgroupService, TaskseriesService, UsersService]; diff --git a/frontend/src/api/model/models.ts b/frontend/src/api/model/models.ts index 028c0ee..b09b6a8 100644 --- a/frontend/src/api/model/models.ts +++ b/frontend/src/api/model/models.ts @@ -29,6 +29,7 @@ export * from './simpleStatusResponse'; export * from './taskEntityInfo'; export * from './taskFieldInfo'; export * from './taskOverviewInfo'; +export * from './taskRepeatDayInfo'; export * from './taskScheduleStopResponse'; export * from './taskShortInfo'; export * from './taskTaskgroupInfo'; diff --git a/frontend/src/api/model/ntfyInformation.ts b/frontend/src/api/model/ntfyInformation.ts index 5919525..1803174 100644 --- a/frontend/src/api/model/ntfyInformation.ts +++ b/frontend/src/api/model/ntfyInformation.ts @@ -23,7 +23,7 @@ export interface NtfyInformation { /** * username of ntfy account for publishing news */ - ntfy_user: string; + ntfy_user?: string; /** * token to ntfy useraccount */ diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 61f2d42..080df50 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -91,6 +91,8 @@ import {MatGridListModule} from "@angular/material/grid-list"; import { StopScheduleManuallyComponent } from './dashboard/active-schedule/stop-schedule-manually/stop-schedule-manually.component'; import { ConnectionSettingsComponent } from './user-settings/connection-settings/connection-settings.component'; import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy-settings/ntfy-settings.component'; +import { TaskSeriesCreatorComponent } from './tasks/task-series-creator/task-series-creator.component'; +import {MatStepperModule} from "@angular/material/stepper"; @NgModule({ declarations: [ AppComponent, @@ -138,6 +140,7 @@ import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy- StopScheduleManuallyComponent, ConnectionSettingsComponent, NtfySettingsComponent, + TaskSeriesCreatorComponent, ], imports: [ BrowserModule, @@ -181,6 +184,7 @@ import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy- NgApexchartsModule, MatButtonToggleModule, MatGridListModule, + MatStepperModule, ], providers: [ HttpClientModule, 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 d745f2b..7b87743 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/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 e28a744..d557009 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 @@ -17,6 +17,7 @@ import {TaskEditorData} from "../task-editor/TaskEditorData"; import * as moment from "moment"; import {ScheduleDashboardComponent} from "../../schedules/schedule-dashboard/schedule-dashboard.component"; import {TaskStatusService} from "../task-status.service"; +import {TaskSeriesCreatorComponent} from "../task-series-creator/task-series-creator.component"; @Component({ selector: 'app-task-detail-overview', @@ -158,4 +159,11 @@ export class TaskDetailOverviewComponent implements OnInit { } }) } + + openRepeatingTaskEditor() { + const dialogRef = this.dialog.open(TaskSeriesCreatorComponent, { + data: this.task!, + minWidth: "400px" + }) + } } diff --git a/frontend/src/app/user-settings/connection-settings/ntfy-settings/ntfy-settings.component.ts b/frontend/src/app/user-settings/connection-settings/ntfy-settings/ntfy-settings.component.ts index 3b9e8ee..d7ad51f 100644 --- a/frontend/src/app/user-settings/connection-settings/ntfy-settings/ntfy-settings.component.ts +++ b/frontend/src/app/user-settings/connection-settings/ntfy-settings/ntfy-settings.component.ts @@ -24,7 +24,7 @@ export class NtfySettingsComponent { next: resp => { this.host_formCtrl.setValue(resp.ntfy_host); this.topic_formCtrl.setValue(resp.ntfy_topic); - this.user_formCtrl.setValue(resp.ntfy_user); + this.user_formCtrl.setValue(resp.ntfy_user!); this.token_formCtrl.setValue(resp.ntfy_token!); } }) From 4b9dd4d17c1f577bb7b8ed5e1c30eb91a3f2e42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 09:43:38 +0100 Subject: [PATCH 03/17] Frontend Components for Creating Daily Repeating Tasks --- frontend/src/api/api/taskseries.service.ts | 159 ++++++++++++++++++ frontend/src/api/model/taskRepeatDayInfo.ts | 33 ++++ .../task-series-creator.component.css | 0 .../task-series-creator.component.html | 52 ++++++ .../task-series-creator.component.spec.ts | 21 +++ .../task-series-creator.component.ts | 78 +++++++++ openapi.yaml | 7 +- 7 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 frontend/src/api/api/taskseries.service.ts create mode 100644 frontend/src/api/model/taskRepeatDayInfo.ts create mode 100644 frontend/src/app/tasks/task-series-creator/task-series-creator.component.css create mode 100644 frontend/src/app/tasks/task-series-creator/task-series-creator.component.html create mode 100644 frontend/src/app/tasks/task-series-creator/task-series-creator.component.spec.ts create mode 100644 frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts diff --git a/frontend/src/api/api/taskseries.service.ts b/frontend/src/api/api/taskseries.service.ts new file mode 100644 index 0000000..3ce54fc --- /dev/null +++ b/frontend/src/api/api/taskseries.service.ts @@ -0,0 +1,159 @@ +/** + * API Title + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +import { SimpleStatusResponse } from '../model/models'; +import { TaskRepeatDayInfo } from '../model/models'; + +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class TaskseriesService { + + protected basePath = 'http://127.0.0.1:8080/api'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, + (value as Date).toISOString().substr(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * daily repeating task creation + * Creates a daily repeating task + * @param taskID internal id of taskgroup + * @param taskRepeatDayInfo + * @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 tasksTaskIDTaskseriesDailyPost(taskID: number, taskRepeatDayInfo?: TaskRepeatDayInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskIDTaskseriesDailyPost(taskID: number, taskRepeatDayInfo?: TaskRepeatDayInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDTaskseriesDailyPost(taskID: number, taskRepeatDayInfo?: TaskRepeatDayInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskIDTaskseriesDailyPost(taskID: number, taskRepeatDayInfo?: TaskRepeatDayInfo, 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 tasksTaskIDTaskseriesDailyPost.'); + } + + 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.post(`${this.configuration.basePath}/tasks/${encodeURIComponent(String(taskID))}/taskseries/daily`, + taskRepeatDayInfo, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + +} diff --git a/frontend/src/api/model/taskRepeatDayInfo.ts b/frontend/src/api/model/taskRepeatDayInfo.ts new file mode 100644 index 0000000..31b219e --- /dev/null +++ b/frontend/src/api/model/taskRepeatDayInfo.ts @@ -0,0 +1,33 @@ +/** + * API Title + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +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/app/tasks/task-series-creator/task-series-creator.component.css b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.css new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..c9282b0 --- /dev/null +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.html @@ -0,0 +1,52 @@ +

Create Task-Series

+ + +
+ Select Task Repeating Strategy + + Repeating Strategy + + Daily + Weekly + + +
+ +
+
+
+ +
+ Define Repeating Information + + Offset + + + + Deadline Strategy + + Fit Next Start + Equal Same Start + + +
+ +
+
+
+ +
+ When should the TaskSerie end? + + Choose a date + + MM/DD/YYYY + + + +
+ +
+
+
+
diff --git a/frontend/src/app/tasks/task-series-creator/task-series-creator.component.spec.ts b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.spec.ts new file mode 100644 index 0000000..00686e8 --- /dev/null +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TaskSeriesCreatorComponent } from './task-series-creator.component'; + +describe('TaskSeriesCreatorComponent', () => { + let component: TaskSeriesCreatorComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TaskSeriesCreatorComponent] + }); + fixture = TestBed.createComponent(TaskSeriesCreatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000..3779531 --- /dev/null +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts @@ -0,0 +1,78 @@ +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"; + + +@Component({ + selector: 'app-task-series-creator', + templateUrl: './task-series-creator.component.html', + styleUrls: ['./task-series-creator.component.css'] +}) +export class TaskSeriesCreatorComponent { + + firstFormGroup = this._formBuilder.group({ + repeatingStrategyCtrl: ['', Validators.required] + }) + + dailyFormGroup = this._formBuilder.group({ + offsetCtrl: ['', Validators.required], + deadlineStrategyCtrl: ['', Validators.required] + }) + + weeklyFormGroup = this._formBuilder.group({ + + }) + + endDateFormGroup = this._formBuilder.group({ + endDateCtrl: ['', Validators.required] + }) + + repeatingStrategy: "DAILY"|"WEEKLY"|undefined = undefined + + constructor(private _formBuilder: FormBuilder, + private taskSeriesService: TaskseriesService, + @Inject(MAT_DIALOG_DATA) public task: TaskEntityInfo, + private dialogRef: MatDialogRef) { + + } + + onSelectRepeatingStrategy() { + const selectedStrategy = this.firstFormGroup.get("repeatingStrategyCtrl")!.value! + if(selectedStrategy === "DAILY") { + this.repeatingStrategy = "DAILY"; + } else { + this.repeatingStrategy = "WEEKLY"; + } + console.log(this.repeatingStrategy) + } + + save() { + if(this.repeatingStrategy === 'DAILY') { + this.saveDailyRepeating() + } + } + + saveDailyRepeating() { + 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 => { + this.dialogRef.close(); + } + }) + } + + convertDeadlineStrategyCtrlToDeadlineEnum() { + const deadlineStrategy = this.dailyFormGroup.get('deadlineStrategyCtrl')!.value; + if(deadlineStrategy === DeadlineStrategyEnum.EqualStart) { + return DeadlineStrategyEnum.EqualStart; + } else { + return DeadlineStrategyEnum.FitStart; + } + } +} diff --git a/openapi.yaml b/openapi.yaml index b83f363..17e44dc 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2830,6 +2830,7 @@ components: required: - offset - deadlineStrategy + - endingDate additionalProperties: false properties: offset: @@ -2841,4 +2842,8 @@ components: type: string enum: - DEADLINE_EQUAL_START - - DEADLINE_FIT_START \ No newline at end of file + - DEADLINE_FIT_START + endingDate: + type: string + format: date + description: Date until the tasks repeat \ No newline at end of file From 59ee88b5fe5c051bc500f5f201aa927b46e9c798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 09:45:54 +0100 Subject: [PATCH 04/17] Replace index placeholder for root-task --- backend/src/main/java/core/services/TaskSeriesService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 4c6ec67..f4078de 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -27,9 +27,10 @@ public class TaskSeriesService { return ServiceExitCode.INVALID_PARAMETER; } + rootTask.setTaskName(rootTask.getTaskName().replace("${i}", convertIndexToString(1))); LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); TaskSerie taskSerie = new TaskSerie(); - int index = 1; + int index = 2; while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { Task task = Task.cloneTask(rootTask); task.setStartDate(currentTaskDate); From c093720a07810c246c01240558e0cfb0926974fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 15:08:24 +0100 Subject: [PATCH 05/17] Deleting of Tasks, Taskseries and TaskSerieItems --- .../timemanager/tasks/TaskEntityInfo.java | 7 +- .../timemanager/tasks/TaskOverviewInfo.java | 7 +- .../timemanager/tasks/TaskShortInfo.java | 8 ++- .../timemanager/tasks/TaskTaskgroupInfo.java | 7 +- .../java/core/entities/timemanager/Task.java | 15 ++--- .../core/entities/timemanager/TaskSerie.java | 18 ++++-- .../entities/timemanager/TaskSerieItem.java | 64 +++++++++++++++++++ .../timemanager/TaskSerieItemRepository.java | 9 +++ .../java/core/services/TaskSeriesService.java | 37 +++++++++-- .../main/java/core/services/TaskService.java | 14 ++-- 10 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 backend/src/main/java/core/entities/timemanager/TaskSerieItem.java create mode 100644 backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java 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 589cc3c..32cc1f5 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 @@ -2,6 +2,7 @@ package core.api.models.timemanager.tasks; import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo; import core.entities.timemanager.Task; +import core.services.TaskSeriesService; import java.time.LocalDate; @@ -30,7 +31,11 @@ public class TaskEntityInfo { public TaskEntityInfo(Task task) { this.taskID = task.getTaskID(); - this.taskName = task.getTaskName(); + if(task.getTaskSerieItem() != null) { + this.taskName = TaskSeriesService.insertNameIndex(task.getTaskSerieItem().getSeriesIndex(), task.getTaskName()); + } else { + this.taskName = task.getTaskName(); + } this.eta = task.getEta(); this.startDate = task.getStartDate(); this.deadline = task.getDeadline(); 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 be7df3a..18ec648 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 @@ -3,6 +3,7 @@ package core.api.models.timemanager.tasks; import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo; import core.entities.timemanager.Task; import core.entities.timemanager.Taskgroup; +import core.services.TaskSeriesService; import java.time.LocalDate; import java.util.List; @@ -20,7 +21,11 @@ public class TaskOverviewInfo { public TaskOverviewInfo(Task task) { this.taskID = task.getTaskID(); - this.taskName = task.getTaskName(); + if(task.getTaskSerieItem() != null) { + this.taskName = TaskSeriesService.insertNameIndex(task.getTaskSerieItem().getSeriesIndex(), task.getTaskName()); + } else { + this.taskName = task.getTaskName(); + } this.activeMinutes = task.getWorkTime(); this.eta = task.getEta(); this.limit = task.getDeadline(); diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/TaskShortInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/TaskShortInfo.java index e422128..140ad71 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/TaskShortInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/TaskShortInfo.java @@ -1,6 +1,7 @@ package core.api.models.timemanager.tasks; import core.entities.timemanager.Task; +import core.services.TaskSeriesService; public class TaskShortInfo { @@ -11,7 +12,12 @@ public class TaskShortInfo { public TaskShortInfo(Task task) { this.taskID = task.getTaskID(); - this.taskName = task.getTaskName(); + if(task.getTaskSerieItem() != null) { + this.taskName = TaskSeriesService.insertNameIndex(task.getTaskSerieItem().getSeriesIndex(), task.getTaskName()); + } else { + this.taskName = task.getTaskName(); + } + this.finishable = task.isFinishable(); } diff --git a/backend/src/main/java/core/api/models/timemanager/tasks/TaskTaskgroupInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/TaskTaskgroupInfo.java index e82bba7..e8b7e00 100644 --- a/backend/src/main/java/core/api/models/timemanager/tasks/TaskTaskgroupInfo.java +++ b/backend/src/main/java/core/api/models/timemanager/tasks/TaskTaskgroupInfo.java @@ -3,6 +3,7 @@ package core.api.models.timemanager.tasks; import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo; import core.entities.timemanager.Task; import core.entities.timemanager.Taskgroup; +import core.services.TaskSeriesService; import java.time.LocalDate; import java.util.ArrayList; @@ -31,7 +32,11 @@ public class TaskTaskgroupInfo { public TaskTaskgroupInfo(Task task) { this.taskID = task.getTaskID(); - this.taskName = task.getTaskName(); + if(task.getTaskSerieItem() != null) { + this.taskName = TaskSeriesService.insertNameIndex(task.getTaskSerieItem().getSeriesIndex(), task.getTaskName()); + } else { + this.taskName = task.getTaskName(); + } this.eta = task.getEta(); this.startDate = task.getStartDate(); this.deadline = task.getDeadline(); diff --git a/backend/src/main/java/core/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index 728101b..dbb92c5 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -30,9 +30,8 @@ public class Task { @OneToMany(mappedBy = "task", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) private List basicTaskSchedules; private int workTime; - @ManyToOne - @JoinColumn(referencedColumnName = "taskSerieID") - private TaskSerie taskSerie; + @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) + private TaskSerieItem taskSerieItem; public Task() { this.basicTaskSchedules = new ArrayList<>(); @@ -131,12 +130,12 @@ public class Task { this.taskID = taskID; } - public TaskSerie getTaskSerie() { - return taskSerie; + public TaskSerieItem getTaskSerieItem() { + return taskSerieItem; } - public void setTaskSerie(TaskSerie taskSerie) { - this.taskSerie = taskSerie; + public void setTaskSerieItem(TaskSerieItem taskSerieItem) { + this.taskSerieItem = taskSerieItem; } public List getBasicTaskSchedules() { @@ -155,7 +154,7 @@ public class Task { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Task task = (Task) o; - return taskID == task.taskID && eta == task.eta && finished == task.finished && workTime == task.workTime && Objects.equals(taskgroup, task.taskgroup) && Objects.equals(taskName, task.taskName) && Objects.equals(startDate, task.startDate) && Objects.equals(deadline, task.deadline); + return taskID == task.taskID; } @Override diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerie.java b/backend/src/main/java/core/entities/timemanager/TaskSerie.java index f317f34..bc12ba1 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerie.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerie.java @@ -12,8 +12,8 @@ public class TaskSerie { @GeneratedValue(strategy = GenerationType.AUTO) private long taskSerieID; - @OneToMany(orphanRemoval = false, cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "taskSerie") - List tasks = new ArrayList<>(); + @OneToMany(fetch = FetchType.EAGER, mappedBy = "taskSerie") + List tasks = new ArrayList<>(); @@ -22,15 +22,21 @@ public class TaskSerie { return taskSerieID; } - public List getTasks() { + public void setTaskSerieID(long taskSerieID) { + this.taskSerieID = taskSerieID; + } + + public List getTasks() { return tasks; } - public void setTasks(List tasks) { + public void setTasks(List tasks) { this.tasks = tasks; } - public void addTask(Task task) { - this.tasks.add(task); + public TaskSerieItem addTask(Task task) { + TaskSerieItem taskSerieItem = new TaskSerieItem(this, task, this.tasks.size()+1); + this.tasks.add(taskSerieItem); + return taskSerieItem; } } diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java new file mode 100644 index 0000000..f5d40fc --- /dev/null +++ b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java @@ -0,0 +1,64 @@ +package core.entities.timemanager; + +import javax.persistence.*; + +@Entity +@Table(name = "task_series_items") +public class TaskSerieItem { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long itemID; + + @ManyToOne() + @JoinColumn(referencedColumnName = "taskSerieID") + private TaskSerie taskSerie; + + @OneToOne + @JoinColumn(name = "task_id") + private Task task; + + + private int seriesIndex; + + public TaskSerieItem(TaskSerie taskSerie, Task task, int index) { + this.taskSerie = taskSerie; + this.seriesIndex = index; + this.task = task; + } + + public TaskSerieItem() { + } + + public long getItemID() { + return itemID; + } + + public void setItemID(long itemID) { + this.itemID = itemID; + } + + public TaskSerie getTaskSerie() { + return taskSerie; + } + + public void setTaskSerie(TaskSerie taskSerie) { + this.taskSerie = taskSerie; + } + + public int getSeriesIndex() { + return seriesIndex; + } + + public void setSeriesIndex(int seriesIndex) { + this.seriesIndex = seriesIndex; + } + + public Task getTask() { + return task; + } + + public void setTask(Task task) { + this.task = task; + } +} diff --git a/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java new file mode 100644 index 0000000..0397bc7 --- /dev/null +++ b/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java @@ -0,0 +1,9 @@ +package core.repositories.timemanager; + +import core.entities.timemanager.TaskSerieItem; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TaskSerieItemRepository extends CrudRepository { +} diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index f4078de..e0e1a9a 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -5,18 +5,25 @@ import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatDayInfo; import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekInfo; import core.entities.timemanager.Task; import core.entities.timemanager.TaskSerie; +import core.entities.timemanager.TaskSerieItem; import core.repositories.timemanager.TaskRepository; +import core.repositories.timemanager.TaskSerieItemRepository; import core.repositories.timemanager.TaskSeriesRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; @Service public class TaskSeriesService { @Autowired private TaskRepository taskRepository; @Autowired private TaskSeriesRepository taskSeriesRepository; + @Autowired private TaskSerieItemRepository taskSerieItemRepository; + + public ServiceExitCode createTaskSeries(Task rootTask, TaskRepeatWeekInfo taskRepeatInfo) { return ServiceExitCode.OK; @@ -27,9 +34,13 @@ public class TaskSeriesService { return ServiceExitCode.INVALID_PARAMETER; } - rootTask.setTaskName(rootTask.getTaskName().replace("${i}", convertIndexToString(1))); + List taskList = new ArrayList<>(); + taskList.add(rootTask); LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); TaskSerie taskSerie = new TaskSerie(); + + TaskSerieItem rootItem = taskSerie.addTask(rootTask); + rootTask.setTaskSerieItem(rootItem); int index = 2; while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { Task task = Task.cloneTask(rootTask); @@ -39,18 +50,30 @@ public class TaskSeriesService { } else if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_FIT_START) { task.setDeadline(currentTaskDate.plusDays(taskRepeatInfo.getOffset()-1)); } - taskSerie.addTask(task); - task.setTaskSerie(taskSerie); - task.setTaskName(task.getTaskName().replace("${i}", convertIndexToString(index))); + TaskSerieItem taskSerieItem = taskSerie.addTask(task); + taskList.add(task); + task.setTaskSerieItem(taskSerieItem); currentTaskDate = currentTaskDate.plusDays(taskRepeatInfo.getOffset()); index++; } taskSeriesRepository.save(taskSerie); - taskRepository.saveAll(taskSerie.getTasks()); + taskRepository.saveAll(taskList); + taskSerieItemRepository.saveAll(taskSerie.getTasks()); + return ServiceExitCode.OK; } + public void deleteTaskSeriesItem(Task task) { + TaskSerieItem item = task.getTaskSerieItem(); + TaskSerie taskSerie = task.getTaskSerieItem().getTaskSerie(); + task.setTaskSerieItem(null); + taskSerieItemRepository.delete(item); + if(taskSerie.getTasks().size() == 1) { + taskSeriesRepository.delete(taskSerie); + } + } + public static String convertIndexToString(int index) { if(index < 10) { return "0" + index; @@ -58,4 +81,8 @@ public class TaskSeriesService { return String.valueOf(index); } } + + public static String insertNameIndex(int seriesIndex, String taskName) { + return taskName.replaceAll("\\$\\{i}", convertIndexToString(seriesIndex)); + } } diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index 73f9336..8677e1d 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -6,6 +6,8 @@ import core.entities.timemanager.AbstractSchedule; import core.entities.timemanager.Task; import core.entities.timemanager.Taskgroup; import core.repositories.timemanager.TaskRepository; +import core.repositories.timemanager.TaskSerieItemRepository; +import core.repositories.timemanager.TaskSeriesRepository; import core.repositories.timemanager.TaskgroupRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -20,13 +22,17 @@ public class TaskService { private final TaskScheduleService taskScheduleService; private final TaskgroupRepository taskgroupRepository; + private final TaskSeriesService taskSeriesService; + public TaskService(@Autowired TaskRepository taskRepository, @Autowired TaskScheduleService taskScheduleService, - TaskgroupRepository taskgroupRepository) { + @Autowired TaskgroupRepository taskgroupRepository, + @Autowired TaskSeriesService taskSeriesService) { this.taskRepository = taskRepository; this.taskScheduleService = taskScheduleService; this.taskgroupRepository = taskgroupRepository; + this.taskSeriesService = taskSeriesService; } public ServiceResult createTask(Taskgroup taskgroup, TaskFieldInfo taskFieldInfo) { @@ -86,13 +92,11 @@ public class TaskService { } public void deleteTask(Task task) { - //taskScheduleService.deleteScheduleByTask(task); - System.err.println(task.getTaskID()); task.getTaskgroup().getTasks().remove(task); taskgroupRepository.save(task.getTaskgroup()); - task.setTaskgroup(null); - taskRepository.save(task); + taskSeriesService.deleteTaskSeriesItem(task); taskRepository.delete(task); + } public void clearTasks(Taskgroup taskgroup) { From e102b91a1f3a8bd15b10d08f1ad7d366082d491a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 15:16:31 +0100 Subject: [PATCH 06/17] Fix Deleting Tasks with TaskSerieItem: Foreign Key Constraint failed --- .../main/java/core/entities/timemanager/TaskSerieItem.java | 4 ++-- backend/src/main/java/core/services/TaskSeriesService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java index f5d40fc..46728fa 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java @@ -10,11 +10,11 @@ public class TaskSerieItem { @GeneratedValue(strategy = GenerationType.AUTO) private long itemID; - @ManyToOne() + @ManyToOne @JoinColumn(referencedColumnName = "taskSerieID") private TaskSerie taskSerie; - @OneToOne + @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "task_id") private Task task; diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index e0e1a9a..efac5cb 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -69,7 +69,7 @@ public class TaskSeriesService { TaskSerie taskSerie = task.getTaskSerieItem().getTaskSerie(); task.setTaskSerieItem(null); taskSerieItemRepository.delete(item); - if(taskSerie.getTasks().size() == 1) { + if(taskSerie.getTasks().size() <= 1) { taskSeriesRepository.delete(taskSerie); } } From 717e8d4e459664c902e975f7b13713ea2d9f5212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 15:32:31 +0100 Subject: [PATCH 07/17] Fix storing multiple TaskSerieItems for RootTask --- .../java/core/entities/timemanager/TaskSerieItem.java | 2 +- .../main/java/core/services/TaskSeriesService.java | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java index 46728fa..3160a78 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java @@ -14,7 +14,7 @@ public class TaskSerieItem { @JoinColumn(referencedColumnName = "taskSerieID") private TaskSerie taskSerie; - @OneToOne(cascade = CascadeType.ALL) + @OneToOne @JoinColumn(name = "task_id") private Task task; diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index efac5cb..65fa3b1 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -35,12 +35,11 @@ public class TaskSeriesService { } List taskList = new ArrayList<>(); - taskList.add(rootTask); - LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); TaskSerie taskSerie = new TaskSerie(); - TaskSerieItem rootItem = taskSerie.addTask(rootTask); rootTask.setTaskSerieItem(rootItem); + + LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); int index = 2; while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { Task task = Task.cloneTask(rootTask); @@ -69,7 +68,11 @@ public class TaskSeriesService { TaskSerie taskSerie = task.getTaskSerieItem().getTaskSerie(); task.setTaskSerieItem(null); taskSerieItemRepository.delete(item); - if(taskSerie.getTasks().size() <= 1) { + if(taskSerie.getTasks().isEmpty()) { + for(TaskSerieItem taskSerieItem : taskSerie.getTasks()) { + taskSerieItem.setTaskSerie(null); + } + taskSerie.getTasks().clear(); taskSeriesRepository.delete(taskSerie); } } From ec4a1cfbc708453afcae1f250fb46e49d2d4d94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 16:49:31 +0100 Subject: [PATCH 08/17] Delete Unreferenced TaskSeries --- .../java/core/entities/timemanager/TaskSerie.java | 2 +- .../repositories/timemanager/TaskRepository.java | 1 + .../timemanager/TaskSerieItemRepository.java | 10 ++++++++++ .../timemanager/TaskSeriesRepository.java | 14 ++++++++++++++ .../main/java/core/services/TaskSeriesService.java | 8 ++++++-- .../src/main/java/core/services/TaskService.java | 1 + 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerie.java b/backend/src/main/java/core/entities/timemanager/TaskSerie.java index bc12ba1..50be1c7 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerie.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerie.java @@ -12,7 +12,7 @@ public class TaskSerie { @GeneratedValue(strategy = GenerationType.AUTO) private long taskSerieID; - @OneToMany(fetch = FetchType.EAGER, mappedBy = "taskSerie") + @OneToMany(fetch = FetchType.EAGER, mappedBy = "taskSerie", orphanRemoval = true) List tasks = new ArrayList<>(); diff --git a/backend/src/main/java/core/repositories/timemanager/TaskRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskRepository.java index d4f7268..208dbe6 100644 --- a/backend/src/main/java/core/repositories/timemanager/TaskRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/TaskRepository.java @@ -20,6 +20,7 @@ public interface TaskRepository extends CrudRepository { @Modifying @Transactional + @Query(value = "DELETE FROM Task t WHERE t.taskgroup = ?1") void deleteAllByTaskgroup(Taskgroup taskgroup); @Transactional diff --git a/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java index 0397bc7..73e9008 100644 --- a/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java @@ -1,9 +1,19 @@ package core.repositories.timemanager; import core.entities.timemanager.TaskSerieItem; +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; + @Repository public interface TaskSerieItemRepository extends CrudRepository { + + @Query(value = "DELETE FROM TaskSerieItem tsi WHERE tsi.task IN (SELECT t FROM Task t WHERE t.taskgroup = ?1)") + @Modifying + @Transactional + void deleteByTaskgroup(Taskgroup taskgroup); } diff --git a/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java b/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java index d03fc42..c0b7301 100644 --- a/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java +++ b/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java @@ -1,9 +1,23 @@ package core.repositories.timemanager; import core.entities.timemanager.TaskSerie; +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.data.repository.query.Param; import org.springframework.stereotype.Repository; +import javax.transaction.Transactional; +import java.util.List; + @Repository public interface TaskSeriesRepository extends CrudRepository { + @Query("SELECT DISTINCT ts FROM TaskSerie ts JOIN ts.tasks tsi JOIN tsi.task t WHERE t.taskgroup = :taskgroup") + List findByTaskgroup(@Param("taskgroup")Taskgroup taskgroup); + + @Modifying + @Transactional + @Query(value = "DELETE FROM TaskSerie ts WHERE ts.tasks.size = 0") + void deleteUnreferenced(); } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 65fa3b1..8c5614b 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -6,6 +6,7 @@ import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekInfo; import core.entities.timemanager.Task; import core.entities.timemanager.TaskSerie; import core.entities.timemanager.TaskSerieItem; +import core.entities.timemanager.Taskgroup; import core.repositories.timemanager.TaskRepository; import core.repositories.timemanager.TaskSerieItemRepository; import core.repositories.timemanager.TaskSeriesRepository; @@ -40,7 +41,6 @@ public class TaskSeriesService { rootTask.setTaskSerieItem(rootItem); LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); - int index = 2; while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { Task task = Task.cloneTask(rootTask); task.setStartDate(currentTaskDate); @@ -53,7 +53,6 @@ public class TaskSeriesService { taskList.add(task); task.setTaskSerieItem(taskSerieItem); currentTaskDate = currentTaskDate.plusDays(taskRepeatInfo.getOffset()); - index++; } taskSeriesRepository.save(taskSerie); @@ -77,6 +76,11 @@ public class TaskSeriesService { } } + public void deleteTaskSerieByTaskgroup(Taskgroup taskgroup) { + taskSerieItemRepository.deleteByTaskgroup(taskgroup); + taskSeriesRepository.deleteUnreferenced(); + } + public static String convertIndexToString(int index) { if(index < 10) { return "0" + index; diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index 8677e1d..d3b4e4a 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -100,6 +100,7 @@ public class TaskService { } public void clearTasks(Taskgroup taskgroup) { + taskSeriesService.deleteTaskSerieByTaskgroup(taskgroup); taskRepository.deleteAllByTaskgroup(taskgroup); } From 015d82587c0a7e8a73d6ccd695b84ef50fece1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 16:58:51 +0100 Subject: [PATCH 09/17] Repair Indexing after Deleting Tasks of Taskserie --- .../entities/timemanager/TaskSerieItem.java | 14 +++++++++++++ .../java/core/services/TaskSeriesService.java | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java index 3160a78..533a931 100644 --- a/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java +++ b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java @@ -1,6 +1,7 @@ package core.entities.timemanager; import javax.persistence.*; +import java.util.Objects; @Entity @Table(name = "task_series_items") @@ -61,4 +62,17 @@ public class TaskSerieItem { public void setTask(Task task) { this.task = task; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TaskSerieItem that = (TaskSerieItem) o; + return itemID == that.itemID; + } + + @Override + public int hashCode() { + return Objects.hash(itemID); + } } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 8c5614b..5952dab 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; @Service @@ -65,6 +66,7 @@ public class TaskSeriesService { public void deleteTaskSeriesItem(Task task) { TaskSerieItem item = task.getTaskSerieItem(); TaskSerie taskSerie = task.getTaskSerieItem().getTaskSerie(); + taskSerie.getTasks().remove(item); task.setTaskSerieItem(null); taskSerieItemRepository.delete(item); if(taskSerie.getTasks().isEmpty()) { @@ -73,9 +75,27 @@ public class TaskSeriesService { } taskSerie.getTasks().clear(); taskSeriesRepository.delete(taskSerie); + } else { + repearIndexing(taskSerie); } } + private void repearIndexing(TaskSerie taskSerie) { + 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); + updatedItems.add(taskSerieItem); + } + + currentIndex++; + } + + taskSerieItemRepository.saveAll(updatedItems); + } + public void deleteTaskSerieByTaskgroup(Taskgroup taskgroup) { taskSerieItemRepository.deleteByTaskgroup(taskgroup); taskSeriesRepository.deleteUnreferenced(); From 7d24ed12296a808c71906a18e7195441b1f03008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Fri, 15 Mar 2024 18:07:40 +0100 Subject: [PATCH 10/17] Implement Generating Repeating Task on Weekly Basis --- .../api/controller/TaskSeriesController.java | 13 ++---- .../repeatinginfo/TaskRepeatWeekDayInfo.java | 30 ++++++++++++- .../repeatinginfo/TaskRepeatWeekInfo.java | 6 ++- .../java/core/services/TaskSeriesService.java | 45 ++++++++++++++++++- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/backend/src/main/java/core/api/controller/TaskSeriesController.java b/backend/src/main/java/core/api/controller/TaskSeriesController.java index c2d2994..bc0238b 100644 --- a/backend/src/main/java/core/api/controller/TaskSeriesController.java +++ b/backend/src/main/java/core/api/controller/TaskSeriesController.java @@ -21,15 +21,10 @@ public class TaskSeriesController { @Autowired private TaskService taskService; @Autowired private TaskSeriesService taskSeriesService; - @PostMapping("/tasks/{taskID}/taskseries/weekly") - public ResponseEntity onCreateTaskSeries(@PathVariable long taskID, @Valid @RequestBody TaskRepeatWeekInfo taskRepeatWeekInfo) { - var taskPermission = taskService.getTaskPermissions(taskID, SecurityContextHolder.getContext().getAuthentication().getName()); - if(taskPermission.hasIssue()) { - return taskPermission.mapToResponseEntity(); - } else { - ServiceExitCode serviceExitCode = taskSeriesService.createTaskSeries(taskPermission.getResult(), taskRepeatWeekInfo); - return serviceExitCode.mapToResponseEntity(); - } + @PostMapping("/tasks/taskseries/weekly") + public ResponseEntity onCreateTaskSeries(@Valid @RequestBody TaskRepeatWeekInfo taskRepeatWeekInfo) { + ServiceExitCode serviceExitCode = taskSeriesService.createTaskSeries(taskRepeatWeekInfo); + return serviceExitCode.mapToResponseEntity(); } @PostMapping("/tasks/{taskID}/taskseries/daily") 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 8589f14..0466638 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 @@ -1,9 +1,37 @@ package core.api.models.timemanager.tasks.repeatinginfo; +import core.api.models.timemanager.tasks.TaskEntityInfo; +import core.entities.timemanager.Task; + +import java.time.DayOfWeek; + public class TaskRepeatWeekDayInfo { - private int weekday; + private DayOfWeek dayOfWeek; private int offset; + private long taskID; + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public long getTaskID() { + return taskID; + } + + public void setTaskID(long taskID) { + this.taskID = taskID; + } } 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 19a4ce8..fc5301f 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 @@ -1,5 +1,6 @@ package core.api.models.timemanager.tasks.repeatinginfo; +import com.fasterxml.jackson.annotation.JsonFormat; import org.hibernate.validator.constraints.Length; import java.time.LocalDate; @@ -9,10 +10,11 @@ public class TaskRepeatWeekInfo { @Length(min = 1, max = 7) private List weekDayInfos; - private DeadlineStrategy deadlineStrategy; - + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") private LocalDate endDate; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private LocalDate startDate; public List getWeekDayInfos() { return weekDayInfos; diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 5952dab..7541fd1 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -2,6 +2,7 @@ package core.services; import core.api.models.timemanager.tasks.repeatinginfo.DeadlineStrategy; import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatDayInfo; +import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekDayInfo; import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekInfo; import core.entities.timemanager.Task; import core.entities.timemanager.TaskSerie; @@ -13,10 +14,12 @@ import core.repositories.timemanager.TaskSeriesRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.time.Duration; import java.time.LocalDate; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Optional; @Service public class TaskSeriesService { @@ -26,8 +29,48 @@ public class TaskSeriesService { @Autowired private TaskSerieItemRepository taskSerieItemRepository; + public ServiceExitCode createTaskSeries(TaskRepeatWeekInfo taskRepeatInfo) { + List createdTasks = new ArrayList<>(); + TaskSerie taskSerie = new TaskSerie(); + for(TaskRepeatWeekDayInfo taskRepeatDayInfo : taskRepeatInfo.getWeekDayInfos()) { + Optional task = taskRepository.findById(taskRepeatDayInfo.getTaskID()); + if(task.isEmpty()) return ServiceExitCode.MISSING_ENTITY; + + 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); + } + } + + taskSerie.getTasks().sort(Comparator.comparing(o -> o.getTask().getStartDate())); + for(int i=0; i Date: Sat, 16 Mar 2024 09:30:48 +0100 Subject: [PATCH 11/17] Frontend Form for generating repeating tasks weekly --- .../repeatinginfo/TaskRepeatWeekDayInfo.java | 10 --- .../repeatinginfo/TaskRepeatWeekInfo.java | 5 +- .../java/core/services/TaskSeriesService.java | 7 +- .../main/java/core/services/TaskService.java | 4 -- frontend/src/api/.openapi-generator/FILES | 2 + frontend/src/api/api/taskseries.service.ts | 67 +++++++++++++++++ frontend/src/api/model/models.ts | 2 + frontend/src/app/app.module.ts | 2 + .../task-dashboard.component.css | 2 +- .../task-dashboard.component.html | 20 +++++- .../task-dashboard.component.ts | 60 +++++++++++++--- .../task-series-creator.component.html | 19 +---- .../task-series-creator.component.ts | 26 ------- openapi.yaml | 72 ++++++++++++++++++- 14 files changed, 224 insertions(+), 74 deletions(-) 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 0466638..4330b2b 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 @@ -6,19 +6,9 @@ import core.entities.timemanager.Task; import java.time.DayOfWeek; public class TaskRepeatWeekDayInfo { - - private DayOfWeek dayOfWeek; private int offset; private long taskID; - public DayOfWeek getDayOfWeek() { - return dayOfWeek; - } - - public void setDayOfWeek(DayOfWeek dayOfWeek) { - this.dayOfWeek = dayOfWeek; - } - public int getOffset() { return offset; } 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 fc5301f..39b4b22 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 @@ -3,18 +3,17 @@ package core.api.models.timemanager.tasks.repeatinginfo; import com.fasterxml.jackson.annotation.JsonFormat; import org.hibernate.validator.constraints.Length; +import javax.validation.constraints.Size; import java.time.LocalDate; import java.util.List; public class TaskRepeatWeekInfo { - @Length(min = 1, max = 7) + @Size(min = 1, max = 7) private List weekDayInfos; private DeadlineStrategy deadlineStrategy; @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") private LocalDate endDate; - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - private LocalDate startDate; public List getWeekDayInfos() { return weekDayInfos; diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index 7541fd1..ac7bb54 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -36,6 +36,9 @@ public class TaskSeriesService { Optional task = taskRepository.findById(taskRepeatDayInfo.getTaskID()); if(task.isEmpty()) return ServiceExitCode.MISSING_ENTITY; + TaskSerieItem rootItem = taskSerie.addTask(task.get()); + task.get().setTaskSerieItem(rootItem); + LocalDate currentTaskDate = task.get().getStartDate().plusDays(taskRepeatDayInfo.getOffset()); while(currentTaskDate.isBefore(taskRepeatInfo.getEndDate())) { Task clonedTask = Task.cloneTask(task.get()); @@ -44,12 +47,14 @@ public class TaskSeriesService { TaskSerieItem taskSerieItem = taskSerie.addTask(clonedTask); clonedTask.setTaskSerieItem(taskSerieItem); createdTasks.add(clonedTask); + + currentTaskDate = currentTaskDate.plusDays(taskRepeatDayInfo.getOffset()); } } taskSerie.getTasks().sort(Comparator.comparing(o -> o.getTask().getStartDate())); for(int i=0; i createTask(Taskgroup taskgroup, TaskFieldInfo taskFieldInfo) { - if(existTaskByName(taskgroup.getTasks(), taskFieldInfo.getTaskName())) { - return new ServiceResult<>(ServiceExitCode.ENTITY_ALREADY_EXIST); - } - //Check for invalid date (deadline before start if(taskFieldInfo.getStartDate() != null && taskFieldInfo.getDeadline() != null && taskFieldInfo.getDeadline().isBefore(taskFieldInfo.getStartDate())) { diff --git a/frontend/src/api/.openapi-generator/FILES b/frontend/src/api/.openapi-generator/FILES index 3469f4b..8171b54 100644 --- a/frontend/src/api/.openapi-generator/FILES +++ b/frontend/src/api/.openapi-generator/FILES @@ -49,6 +49,8 @@ model/taskEntityInfo.ts model/taskFieldInfo.ts model/taskOverviewInfo.ts model/taskRepeatDayInfo.ts +model/taskRepeatWeekDayInfo.ts +model/taskRepeatWeekInfo.ts model/taskScheduleStopResponse.ts model/taskShortInfo.ts model/taskTaskgroupInfo.ts diff --git a/frontend/src/api/api/taskseries.service.ts b/frontend/src/api/api/taskseries.service.ts index 3ce54fc..722e542 100644 --- a/frontend/src/api/api/taskseries.service.ts +++ b/frontend/src/api/api/taskseries.service.ts @@ -20,6 +20,7 @@ import { Observable } from 'rxjs'; import { SimpleStatusResponse } from '../model/models'; import { TaskRepeatDayInfo } from '../model/models'; +import { TaskRepeatWeekInfo } from '../model/models'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { Configuration } from '../configuration'; @@ -156,4 +157,70 @@ export class TaskseriesService { ); } + /** + * daily repeating task creation + * Creates a daily repeating task + * @param taskRepeatWeekInfo + * @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 tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable; + public tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable>; + public tasksTaskseriesWeeklyPost(taskRepeatWeekInfo?: TaskRepeatWeekInfo, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext}): Observable { + + 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.post(`${this.configuration.basePath}/tasks/taskseries/weekly`, + taskRepeatWeekInfo, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + reportProgress: reportProgress + } + ); + } + } diff --git a/frontend/src/api/model/models.ts b/frontend/src/api/model/models.ts index b09b6a8..12edcd1 100644 --- a/frontend/src/api/model/models.ts +++ b/frontend/src/api/model/models.ts @@ -30,6 +30,8 @@ export * from './taskEntityInfo'; export * from './taskFieldInfo'; export * from './taskOverviewInfo'; export * from './taskRepeatDayInfo'; +export * from './taskRepeatWeekDayInfo'; +export * from './taskRepeatWeekInfo'; export * from './taskScheduleStopResponse'; export * from './taskShortInfo'; export * from './taskTaskgroupInfo'; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 080df50..71ac5f2 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -93,6 +93,7 @@ import { ConnectionSettingsComponent } from './user-settings/connection-settings import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy-settings/ntfy-settings.component'; import { TaskSeriesCreatorComponent } from './tasks/task-series-creator/task-series-creator.component'; import {MatStepperModule} from "@angular/material/stepper"; +import { TaskWeeklySeriesCreatorComponent } from './tasks/task-weekly-series-creator/task-weekly-series-creator.component'; @NgModule({ declarations: [ AppComponent, @@ -141,6 +142,7 @@ import {MatStepperModule} from "@angular/material/stepper"; ConnectionSettingsComponent, NtfySettingsComponent, TaskSeriesCreatorComponent, + TaskWeeklySeriesCreatorComponent, ], imports: [ BrowserModule, diff --git a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css index 532013d..542e3f2 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.css @@ -20,7 +20,7 @@ td, th { margin: 0 auto; } -.mat-column-status, .mat-column-eta { +.mat-column-status, .mat-column-eta, .mat-column-select { width: 32px; text-align: left; } 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 d8d33bc..0358314 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html @@ -8,6 +8,22 @@
+ + + + + + - + 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 df32cb8..e842c3c 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -10,6 +10,8 @@ import {MatSnackBar} from "@angular/material/snack-bar"; import {ClearTaskDialogComponent, ClearTaskDialogData} from "../clear-task-dialog/clear-task-dialog.component"; import * as moment from "moment/moment"; import {TaskStatus, TaskStatusService} from "../task-status.service"; +import {SelectionModel} from "@angular/cdk/collections"; +import {TaskWeeklySeriesCreatorComponent} from "../task-weekly-series-creator/task-weekly-series-creator.component"; @Component({ selector: 'app-task-dashboard', @@ -19,15 +21,7 @@ import {TaskStatus, TaskStatusService} from "../task-status.service"; export class TaskDashboardComponent implements OnChanges{ ngOnChanges(): void { if(this.taskgroupID != undefined) { - this.taskService.tasksTaskgroupIDStatusGet(this.taskgroupID!, "all").subscribe({ - next: resp => { - this.datasource = new MatTableDataSource(resp); - this.datasource.paginator = this.paginator!; - this.datasource.sort = this.sort!; - - resp.forEach(task => console.log(task)) - } - }) + this.fetchTasks() } } @@ -35,9 +29,29 @@ export class TaskDashboardComponent implements OnChanges{ @ViewChild(MatPaginator) paginator: MatPaginator | undefined @ViewChild(MatSort) sort: MatSort | undefined - displayedColumns: string[] = ['status', 'name', 'eta', 'start', 'deadline', 'finished', 'edit', 'delete']; + displayedColumns: string[] = ['select', 'status', 'name', 'eta', 'start', 'deadline', 'finished', 'edit', 'delete']; datasource: MatTableDataSource = new MatTableDataSource(); + selection = new SelectionModel(true, []); + + /** Whether the number of selected elements matches the total number of rows. */ + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.datasource.data.length; + return numSelected === numRows; + } + + /** Selects all rows if they are not all selected; otherwise clear selection. */ + toggleAllRows() { + if (this.isAllSelected()) { + this.selection.clear(); + return; + } + + this.selection.select(...this.datasource.data); + } + + constructor(private taskService: TaskService, private dialog: MatDialog, private snackbar: MatSnackBar, @@ -107,4 +121,30 @@ export class TaskDashboardComponent implements OnChanges{ } protected readonly TaskStatus = TaskStatus; + + repeatSelectedTasks() { + const selectedTasks = this.selection.selected; + const dialogRef = this.dialog.open(TaskWeeklySeriesCreatorComponent, { + data: selectedTasks, + minWidth: "400px" + }); + dialogRef.afterClosed().subscribe(res => { + if(res) { + this.fetchTasks() + } + }) + + } + + fetchTasks() { + this.taskService.tasksTaskgroupIDStatusGet(this.taskgroupID!, "all").subscribe({ + next: resp => { + this.datasource = new MatTableDataSource(resp); + this.datasource.paginator = this.paginator!; + this.datasource.sort = this.sort!; + + resp.forEach(task => console.log(task)) + } + }) + } } 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 c9282b0..e000da5 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 @@ -1,22 +1,7 @@

Create Task-Series

- -
- Select Task Repeating Strategy - - Repeating Strategy - - Daily - Weekly - - -
- -
- -
- -
+ + Define Repeating Information Offset 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 3779531..ed27ba5 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 @@ -13,25 +13,15 @@ import * as moment from "moment"; }) export class TaskSeriesCreatorComponent { - firstFormGroup = this._formBuilder.group({ - repeatingStrategyCtrl: ['', Validators.required] - }) - dailyFormGroup = this._formBuilder.group({ offsetCtrl: ['', Validators.required], deadlineStrategyCtrl: ['', Validators.required] }) - weeklyFormGroup = this._formBuilder.group({ - - }) - endDateFormGroup = this._formBuilder.group({ endDateCtrl: ['', Validators.required] }) - repeatingStrategy: "DAILY"|"WEEKLY"|undefined = undefined - constructor(private _formBuilder: FormBuilder, private taskSeriesService: TaskseriesService, @Inject(MAT_DIALOG_DATA) public task: TaskEntityInfo, @@ -39,23 +29,7 @@ export class TaskSeriesCreatorComponent { } - onSelectRepeatingStrategy() { - const selectedStrategy = this.firstFormGroup.get("repeatingStrategyCtrl")!.value! - if(selectedStrategy === "DAILY") { - this.repeatingStrategy = "DAILY"; - } else { - this.repeatingStrategy = "WEEKLY"; - } - console.log(this.repeatingStrategy) - } - save() { - if(this.repeatingStrategy === 'DAILY') { - this.saveDailyRepeating() - } - } - - saveDailyRepeating() { this.taskSeriesService.tasksTaskIDTaskseriesDailyPost(this.task.taskID,{ offset: Number(this.dailyFormGroup.get('offsetCtrl')!.value!), deadlineStrategy: this.convertDeadlineStrategyCtrlToDeadlineEnum(), diff --git a/openapi.yaml b/openapi.yaml index 17e44dc..4afa66e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2119,6 +2119,38 @@ paths: application/json: schema: $ref: '#/components/schemas/SimpleStatusResponse' + /tasks/taskseries/weekly: + post: + security: + - API_TOKEN: [] + tags: + - taskseries + description: Creates a daily repeating task + summary: daily repeating task creation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TaskRepeatWeekInfo' + 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' components: @@ -2846,4 +2878,42 @@ components: endingDate: type: string format: date - description: Date until the tasks repeat \ No newline at end of file + description: Date until the tasks repeat + TaskRepeatWeekDayInfo: + required: + - offset + - taskID + additionalProperties: false + properties: + offset: + type: number + description: number repeating days + example: 7 + minimum: 1 + taskID: + type: number + description: internal identifier of task + example: 1 + TaskRepeatWeekInfo: + required: + - weekDayInfos + - deadlineStrategy + - endDate + additionalProperties: false + properties: + deadlineStrategy: + type: string + enum: + - DEADLINE_EQUAL_START + - DEADLINE_FIT_START + endDate: + type: string + format: date + description: Date until the tasks repeat + weekDayInfos: + type: array + items: + $ref: '#/components/schemas/TaskRepeatWeekDayInfo' + maxLength: 7 + minLength: 1 + \ No newline at end of file From 49119b2549bebdb95c319de260ba1a4927a2bc0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 09:31:17 +0100 Subject: [PATCH 12/17] Newly files in frontend for generating weekly repeating tasks --- .../src/api/model/taskRepeatWeekDayInfo.ts | 24 ++++++ frontend/src/api/model/taskRepeatWeekInfo.ts | 31 +++++++ .../task-weekly-series-creator.component.css | 10 +++ .../task-weekly-series-creator.component.html | 50 +++++++++++ ...sk-weekly-series-creator.component.spec.ts | 21 +++++ .../task-weekly-series-creator.component.ts | 83 +++++++++++++++++++ 6 files changed, 219 insertions(+) create mode 100644 frontend/src/api/model/taskRepeatWeekDayInfo.ts create mode 100644 frontend/src/api/model/taskRepeatWeekInfo.ts create mode 100644 frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.css create mode 100644 frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.html create mode 100644 frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.spec.ts create mode 100644 frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.ts diff --git a/frontend/src/api/model/taskRepeatWeekDayInfo.ts b/frontend/src/api/model/taskRepeatWeekDayInfo.ts new file mode 100644 index 0000000..ed3390e --- /dev/null +++ b/frontend/src/api/model/taskRepeatWeekDayInfo.ts @@ -0,0 +1,24 @@ +/** + * API Title + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface TaskRepeatWeekDayInfo { + /** + * number repeating days + */ + offset: number; + /** + * internal identifier of task + */ + taskID: number; +} + diff --git a/frontend/src/api/model/taskRepeatWeekInfo.ts b/frontend/src/api/model/taskRepeatWeekInfo.ts new file mode 100644 index 0000000..6803c5c --- /dev/null +++ b/frontend/src/api/model/taskRepeatWeekInfo.ts @@ -0,0 +1,31 @@ +/** + * API Title + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +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-weekly-series-creator/task-weekly-series-creator.component.css b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.css new file mode 100644 index 0000000..a91ad7f --- /dev/null +++ b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.css @@ -0,0 +1,10 @@ +#deadline-strategy-form { + margin-top: 10px; + padding-left: 10px; + padding-right: 10px; +} + +#endDate-form { + padding-left: 10px; + padding-right: 10px; +} 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 new file mode 100644 index 0000000..2d18e9b --- /dev/null +++ b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.html @@ -0,0 +1,50 @@ +

Configure Weekly Repeating Tasks

+ +
+ +
+ + + {{getDayOfWeek(tasks[i].startDate)}} + {{formatDate(tasks[i].startDate)}} + +

Taskname: {{tasks[i].taskName}}

+

Eta: {{tasks[i].eta}} Minutes

+

Startdate: {{formatDate(tasks[i].startDate)}}

+

Deadline: {{formatDate(tasks[i].deadline)}}

+ + Repeating-Offset + + Number of weeks until the task is to be repeated + + + + +
+
+
+
+ + Deadline-Strategy + + + {{deadlineStrategy}} + + + + + + Ending Date + + MM/DD/YYYY + + + + +
+ + +
+ + + diff --git a/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.spec.ts b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.spec.ts new file mode 100644 index 0000000..4c28792 --- /dev/null +++ b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TaskWeeklySeriesCreatorComponent } from './task-weekly-series-creator.component'; + +describe('TaskWeeklySeriesCreatorComponent', () => { + let component: TaskWeeklySeriesCreatorComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TaskWeeklySeriesCreatorComponent] + }); + fixture = TestBed.createComponent(TaskWeeklySeriesCreatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 0000000..9b40131 --- /dev/null +++ b/frontend/src/app/tasks/task-weekly-series-creator/task-weekly-series-creator.component.ts @@ -0,0 +1,83 @@ +import {Component, Inject, OnInit} from '@angular/core'; +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; + + +@Component({ + selector: 'app-task-weekly-series-creator', + templateUrl: './task-weekly-series-creator.component.html', + styleUrls: ['./task-weekly-series-creator.component.css'] +}) +export class TaskWeeklySeriesCreatorComponent implements OnInit{ + + repeatingOffsetForm: FormGroup | undefined + availableDeadlineStrategys: DeadlineStrategyEnum[] = ["DEADLINE_EQUAL_START", "DEADLINE_FIT_START"] + + constructor(private dialogRef: MatDialogRef, + private taskRepeatingService: TaskseriesService, + @Inject(MAT_DIALOG_DATA) public tasks: TaskEntityInfo[], + private formbuilder: FormBuilder) { + } + + ngOnInit(): void { + this.repeatingOffsetForm = this.formbuilder.group({ + offsets: this.formbuilder.array(this.tasks.map(task => this.formbuilder.group({ + offsetCtrl: ['', [Validators.required, Validators.min(1)]] + }))), + deadlineStrategyCtrl: ['', Validators.required], + endingDateCtrl: ['', Validators.required] + + }) + } + + + + getDayOfWeek(date: string) { + return moment(date).format("dddd") + } + + formatDate(date: string): string { + return moment(date).format("dd, DD. MMMM YYYY") + } + + removeTaskFromRepeatingList(task: TaskEntityInfo, formIndex: number) { + this.tasks = this.tasks.filter(t => t.taskID !== task.taskID); + this.deleteOffset(formIndex) + } + + get offsets() { + return this.repeatingOffsetForm!.controls["offsets"] as FormArray + } + + deleteOffset(taskIndex: number) { + this.offsets.removeAt(taskIndex); + } + + saveRepeatingTask() { + const weekDayInfos: TaskRepeatWeekDayInfo[] = [] + const formArrayValues: {offsetCtrl: string}[] = this.offsets.value + for(let i=0; i { + this.dialogRef.close(true) + } + }) + } + + cancel() { + this.dialogRef.close(false); + } +} From 9e5a1337d93f35cddabb07faf0f788762f5a1db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 09:34:17 +0100 Subject: [PATCH 13/17] Avoid weekly task repeatition with no selected tasks --- .../src/app/tasks/task-dashboard/task-dashboard.component.html | 2 +- 1 file changed, 1 insertion(+), 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 0358314..4eb2960 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html @@ -62,7 +62,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 e842c3c..49b5e14 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.ts @@ -48,7 +48,7 @@ export class TaskDashboardComponent implements OnChanges{ return; } - this.selection.select(...this.datasource.data); + this.selection.select(...this.datasource.data.filter(task => !task.hasTaskSerie)); } From e58e6addb9abd32432e50d4a72d04e14b032d775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 10:09:38 +0100 Subject: [PATCH 16/17] Clone Schedules when Repeating Tasks --- .../timemanager/AbstractSchedule.java | 4 +++ .../timemanager/AdvancedTaskSchedule.java | 11 +++++++ .../timemanager/BasicTaskSchedule.java | 10 +++++++ .../java/core/services/TaskSeriesService.java | 29 ++++++++++++++++--- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java b/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java index f7f4de1..a903c23 100644 --- a/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java +++ b/backend/src/main/java/core/entities/timemanager/AbstractSchedule.java @@ -124,4 +124,8 @@ public abstract class AbstractSchedule { return (int) duration.toMinutes(); } } + + public abstract AbstractSchedule cloneSchedule(); + + public abstract void shiftSchedule(long numberDays); } diff --git a/backend/src/main/java/core/entities/timemanager/AdvancedTaskSchedule.java b/backend/src/main/java/core/entities/timemanager/AdvancedTaskSchedule.java index 2289c09..91f1d7c 100644 --- a/backend/src/main/java/core/entities/timemanager/AdvancedTaskSchedule.java +++ b/backend/src/main/java/core/entities/timemanager/AdvancedTaskSchedule.java @@ -71,4 +71,15 @@ public class AdvancedTaskSchedule extends AbstractSchedule { public boolean isMissed(LocalDateTime timeReference) { return startTime == null && scheduleEnd.toLocalDate().isBefore(timeReference.toLocalDate()); } + + @Override + public AbstractSchedule cloneSchedule() { + return new AdvancedTaskSchedule(this.task, this.scheduleStart, this.scheduleEnd); + } + + @Override + public void shiftSchedule(long numberDays) { + this.scheduleStart = this.scheduleStart.plusDays(numberDays); + this.scheduleEnd = this.scheduleEnd.plusDays(numberDays); + } } diff --git a/backend/src/main/java/core/entities/timemanager/BasicTaskSchedule.java b/backend/src/main/java/core/entities/timemanager/BasicTaskSchedule.java index fe6a393..07e13d5 100644 --- a/backend/src/main/java/core/entities/timemanager/BasicTaskSchedule.java +++ b/backend/src/main/java/core/entities/timemanager/BasicTaskSchedule.java @@ -52,4 +52,14 @@ public class BasicTaskSchedule extends AbstractSchedule{ public boolean isMissed(LocalDateTime timeReference) { return startTime == null && scheduleDate.isBefore(timeReference.toLocalDate()); } + + @Override + public AbstractSchedule cloneSchedule() { + return new BasicTaskSchedule(this.task, this.scheduleDate); + } + + @Override + public void shiftSchedule(long numberDays) { + this.scheduleDate = this.scheduleDate.plusDays(numberDays); + } } diff --git a/backend/src/main/java/core/services/TaskSeriesService.java b/backend/src/main/java/core/services/TaskSeriesService.java index ac7bb54..7f1eb85 100644 --- a/backend/src/main/java/core/services/TaskSeriesService.java +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -4,10 +4,8 @@ import core.api.models.timemanager.tasks.repeatinginfo.DeadlineStrategy; import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatDayInfo; import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekDayInfo; import core.api.models.timemanager.tasks.repeatinginfo.TaskRepeatWeekInfo; -import core.entities.timemanager.Task; -import core.entities.timemanager.TaskSerie; -import core.entities.timemanager.TaskSerieItem; -import core.entities.timemanager.Taskgroup; +import core.entities.timemanager.*; +import core.repositories.timemanager.ScheduleRepository; import core.repositories.timemanager.TaskRepository; import core.repositories.timemanager.TaskSerieItemRepository; import core.repositories.timemanager.TaskSeriesRepository; @@ -16,6 +14,7 @@ import org.springframework.stereotype.Service; 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; @@ -27,11 +26,14 @@ public class TaskSeriesService { @Autowired private TaskRepository taskRepository; @Autowired private TaskSeriesRepository taskSeriesRepository; @Autowired private TaskSerieItemRepository taskSerieItemRepository; + @Autowired private ScheduleRepository scheduleRepository; public ServiceExitCode createTaskSeries(TaskRepeatWeekInfo taskRepeatInfo) { List createdTasks = new ArrayList<>(); TaskSerie taskSerie = new TaskSerie(); + List abstractSchedules = new ArrayList<>(); + for(TaskRepeatWeekDayInfo taskRepeatDayInfo : taskRepeatInfo.getWeekDayInfos()) { Optional task = taskRepository.findById(taskRepeatDayInfo.getTaskID()); if(task.isEmpty()) return ServiceExitCode.MISSING_ENTITY; @@ -48,6 +50,7 @@ public class TaskSeriesService { clonedTask.setTaskSerieItem(taskSerieItem); createdTasks.add(clonedTask); + abstractSchedules.addAll(cloneSchedules(task.get(), clonedTask)); currentTaskDate = currentTaskDate.plusDays(taskRepeatDayInfo.getOffset()); } } @@ -75,6 +78,7 @@ public class TaskSeriesService { taskSeriesRepository.save(taskSerie); taskRepository.saveAll(createdTasks); taskSerieItemRepository.saveAll(taskSerie.getTasks()); + scheduleRepository.saveAll(abstractSchedules); return ServiceExitCode.OK; } @@ -85,6 +89,7 @@ public class TaskSeriesService { } List taskList = new ArrayList<>(); + List abstractSchedules = new ArrayList<>(); TaskSerie taskSerie = new TaskSerie(); TaskSerieItem rootItem = taskSerie.addTask(rootTask); rootTask.setTaskSerieItem(rootItem); @@ -101,16 +106,32 @@ public class TaskSeriesService { TaskSerieItem taskSerieItem = taskSerie.addTask(task); taskList.add(task); task.setTaskSerieItem(taskSerieItem); + + abstractSchedules.addAll(cloneSchedules(rootTask, task)); currentTaskDate = currentTaskDate.plusDays(taskRepeatInfo.getOffset()); } taskSeriesRepository.save(taskSerie); taskRepository.saveAll(taskList); taskSerieItemRepository.saveAll(taskSerie.getTasks()); + scheduleRepository.saveAll(abstractSchedules); 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 3e3fba9b2df0d80c676c95abbc96d53f99e6eac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 10:09:52 +0100 Subject: [PATCH 17/17] Delete Tasks that do not belong to any series --- backend/src/main/java/core/services/TaskService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/core/services/TaskService.java b/backend/src/main/java/core/services/TaskService.java index 1b4bc14..292b442 100644 --- a/backend/src/main/java/core/services/TaskService.java +++ b/backend/src/main/java/core/services/TaskService.java @@ -90,7 +90,9 @@ public class TaskService { public void deleteTask(Task task) { task.getTaskgroup().getTasks().remove(task); taskgroupRepository.save(task.getTaskgroup()); - taskSeriesService.deleteTaskSeriesItem(task); + if(task.getTaskSerieItem() != null) { + taskSeriesService.deleteTaskSeriesItem(task); + } taskRepository.delete(task); }
+ + + + + + Name @@ -45,7 +61,9 @@ + + - + From f3c491826b0f3796823d5422fcdb5ccfdd96a7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 09:37:42 +0100 Subject: [PATCH 14/17] Disable daily task repetition when task is already part of a series --- .../api/models/timemanager/tasks/TaskEntityInfo.java | 10 ++++++++++ frontend/src/api/model/taskEntityInfo.ts | 4 ++++ .../task-detail-overview.component.html | 2 +- openapi.yaml | 5 +++++ 4 files changed, 20 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 32cc1f5..bd5e11e 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 @@ -28,6 +28,7 @@ public class TaskEntityInfo { private boolean hasActiveSchedules; private boolean hasPlannedSchedules; + private boolean hasTaskSerie; public TaskEntityInfo(Task task) { this.taskID = task.getTaskID(); @@ -47,6 +48,7 @@ public class TaskEntityInfo { this.finishable = task.isFinishable(); this.hasActiveSchedules = task.hasActiveSchedule(); this.hasPlannedSchedules = task.hasPlannedSchedules(); + this.hasTaskSerie = task.getTaskSerieItem() != null; } public long getTaskID() { @@ -136,4 +138,12 @@ public class TaskEntityInfo { public void setHasPlannedSchedules(boolean hasPlannedSchedules) { this.hasPlannedSchedules = hasPlannedSchedules; } + + public boolean isHasTaskSerie() { + return hasTaskSerie; + } + + public void setHasTaskSerie(boolean hasTaskSerie) { + this.hasTaskSerie = hasTaskSerie; + } } diff --git a/frontend/src/api/model/taskEntityInfo.ts b/frontend/src/api/model/taskEntityInfo.ts index e97b949..3a2b2d0 100644 --- a/frontend/src/api/model/taskEntityInfo.ts +++ b/frontend/src/api/model/taskEntityInfo.ts @@ -56,5 +56,9 @@ export interface TaskEntityInfo { * determines whether the task has schedules that can be started */ hasPlannedSchedules: boolean; + /** + * determines whether the task is associated with a taskserie + */ + hasTaskSerie: 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 7b87743..24038f3 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 4afa66e..2ff5ea4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2414,6 +2414,7 @@ components: - finishable - hasActiveSchedules - hasPlannedSchedules + - hasTaskSerie additionalProperties: false properties: taskID: @@ -2458,6 +2459,10 @@ components: hasPlannedSchedules: type: boolean description: determines whether the task has schedules that can be started + hasTaskSerie: + type: boolean + description: determines whether the task is associated with a taskserie + example: false TaskTaskgroupInfo: required: - taskID From e7283c030df49604256b20083fdc56e6148a5be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=B6ckelmann?= Date: Sat, 16 Mar 2024 09:39:13 +0100 Subject: [PATCH 15/17] Disable weekly task repetition when task is already part of a series --- .../src/app/tasks/task-dashboard/task-dashboard.component.html | 3 ++- .../src/app/tasks/task-dashboard/task-dashboard.component.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 4eb2960..9d04aaa 100644 --- a/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html +++ b/frontend/src/app/tasks/task-dashboard/task-dashboard.component.html @@ -19,7 +19,8 @@ + [checked]="selection.isSelected(row)" + [disabled]="row.hasTaskSerie">