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..bc0238b --- /dev/null +++ b/backend/src/main/java/core/api/controller/TaskSeriesController.java @@ -0,0 +1,40 @@ +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/taskseries/weekly") + public ResponseEntity onCreateTaskSeries(@Valid @RequestBody TaskRepeatWeekInfo taskRepeatWeekInfo) { + ServiceExitCode serviceExitCode = taskSeriesService.createTaskSeries(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/TaskEntityInfo.java b/backend/src/main/java/core/api/models/timemanager/tasks/TaskEntityInfo.java index 589cc3c..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 @@ -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; @@ -27,10 +28,15 @@ public class TaskEntityInfo { private boolean hasActiveSchedules; private boolean hasPlannedSchedules; + private boolean hasTaskSerie; 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(); @@ -42,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() { @@ -131,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/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/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..a188057 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatDayInfo.java @@ -0,0 +1,38 @@ +package core.api.models.timemanager.tasks.repeatinginfo; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; + +public class TaskRepeatDayInfo { + + private int offset; + private DeadlineStrategy deadlineStrategy; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private LocalDate endingDate; + + 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 getEndingDate() { + return endingDate; + } + + public void setEndingDate(LocalDate endingDate) { + this.endingDate = endingDate; + } +} 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..4330b2b --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekDayInfo.java @@ -0,0 +1,27 @@ +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 offset; + private long taskID; + + 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 new file mode 100644 index 0000000..39b4b22 --- /dev/null +++ b/backend/src/main/java/core/api/models/timemanager/tasks/repeatinginfo/TaskRepeatWeekInfo.java @@ -0,0 +1,41 @@ +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 { + + @Size(min = 1, max = 7) + private List weekDayInfos; + private DeadlineStrategy deadlineStrategy; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + 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/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/entities/timemanager/Task.java b/backend/src/main/java/core/entities/timemanager/Task.java index b6bf550..dbb92c5 100644 --- a/backend/src/main/java/core/entities/timemanager/Task.java +++ b/backend/src/main/java/core/entities/timemanager/Task.java @@ -22,21 +22,16 @@ 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; + @OneToOne(mappedBy = "task", cascade = CascadeType.ALL, orphanRemoval = true) + private TaskSerieItem taskSerieItem; public Task() { this.basicTaskSchedules = new ArrayList<>(); @@ -52,6 +47,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 +130,14 @@ public class Task { this.taskID = taskID; } + public TaskSerieItem getTaskSerieItem() { + return taskSerieItem; + } + + public void setTaskSerieItem(TaskSerieItem taskSerieItem) { + this.taskSerieItem = taskSerieItem; + } + public List getBasicTaskSchedules() { if(basicTaskSchedules == null) { return new ArrayList<>(); @@ -140,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 new file mode 100644 index 0000000..50be1c7 --- /dev/null +++ b/backend/src/main/java/core/entities/timemanager/TaskSerie.java @@ -0,0 +1,42 @@ +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(fetch = FetchType.EAGER, mappedBy = "taskSerie", orphanRemoval = true) + List tasks = new ArrayList<>(); + + + + + public long getTaskSerieID() { + return taskSerieID; + } + + public void setTaskSerieID(long taskSerieID) { + this.taskSerieID = taskSerieID; + } + + public List getTasks() { + return tasks; + } + + public void setTasks(List tasks) { + this.tasks = tasks; + } + + 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..533a931 --- /dev/null +++ b/backend/src/main/java/core/entities/timemanager/TaskSerieItem.java @@ -0,0 +1,78 @@ +package core.entities.timemanager; + +import javax.persistence.*; +import java.util.Objects; + +@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; + } + + @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/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 new file mode 100644 index 0000000..73e9008 --- /dev/null +++ b/backend/src/main/java/core/repositories/timemanager/TaskSerieItemRepository.java @@ -0,0 +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 new file mode 100644 index 0000000..c0b7301 --- /dev/null +++ b/backend/src/main/java/core/repositories/timemanager/TaskSeriesRepository.java @@ -0,0 +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/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..7f1eb85 --- /dev/null +++ b/backend/src/main/java/core/services/TaskSeriesService.java @@ -0,0 +1,184 @@ +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.*; +import core.repositories.timemanager.ScheduleRepository; +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.Duration; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +@Service +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; + + 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()); + clonedTask.setStartDate(currentTaskDate); + + TaskSerieItem taskSerieItem = taskSerie.addTask(clonedTask); + clonedTask.setTaskSerieItem(taskSerieItem); + createdTasks.add(clonedTask); + + abstractSchedules.addAll(cloneSchedules(task.get(), clonedTask)); + currentTaskDate = currentTaskDate.plusDays(taskRepeatDayInfo.getOffset()); + } + } + + taskSerie.getTasks().sort(Comparator.comparing(o -> o.getTask().getStartDate())); + for(int i=0; i taskList = new ArrayList<>(); + List abstractSchedules = new ArrayList<>(); + TaskSerie taskSerie = new TaskSerie(); + TaskSerieItem rootItem = taskSerie.addTask(rootTask); + rootTask.setTaskSerieItem(rootItem); + + LocalDate currentTaskDate = rootTask.getStartDate().plusDays(taskRepeatInfo.getOffset()); + while(currentTaskDate.isBefore(taskRepeatInfo.getEndingDate())) { + Task task = Task.cloneTask(rootTask); + task.setStartDate(currentTaskDate); + if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_EQUAL_START) { + task.setDeadline(currentTaskDate); + } else if(taskRepeatInfo.getDeadlineStrategy() == DeadlineStrategy.DEADLINE_FIT_START) { + task.setDeadline(currentTaskDate.plusDays(taskRepeatInfo.getOffset()-1)); + } + TaskSerieItem taskSerieItem = taskSerie.addTask(task); + taskList.add(task); + task.setTaskSerieItem(taskSerieItem); + + 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(); + taskSerie.getTasks().remove(item); + task.setTaskSerieItem(null); + taskSerieItemRepository.delete(item); + if(taskSerie.getTasks().isEmpty()) { + for(TaskSerieItem taskSerieItem : taskSerie.getTasks()) { + taskSerieItem.setTaskSerie(null); + } + 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(); + } + + public static String convertIndexToString(int index) { + if(index < 10) { + return "0" + index; + } else { + 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..292b442 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,20 +22,20 @@ 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) { - 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())) { @@ -86,16 +88,17 @@ 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); + if(task.getTaskSerieItem() != null) { + taskSeriesService.deleteTaskSeriesItem(task); + } taskRepository.delete(task); + } public void clearTasks(Taskgroup taskgroup) { + taskSeriesService.deleteTaskSerieByTaskgroup(taskgroup); taskRepository.deleteAllByTaskgroup(taskgroup); } diff --git a/frontend/src/api/.openapi-generator/FILES b/frontend/src/api/.openapi-generator/FILES index 3595c4b..8171b54 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,9 @@ model/simpleStatusResponse.ts 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.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/api/taskseries.service.ts b/frontend/src/api/api/taskseries.service.ts new file mode 100644 index 0000000..722e542 --- /dev/null +++ b/frontend/src/api/api/taskseries.service.ts @@ -0,0 +1,226 @@ +/** + * 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 { TaskRepeatWeekInfo } 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 + } + ); + } + + /** + * 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 028c0ee..12edcd1 100644 --- a/frontend/src/api/model/models.ts +++ b/frontend/src/api/model/models.ts @@ -29,6 +29,9 @@ export * from './simpleStatusResponse'; 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/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/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/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/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/app.module.ts b/frontend/src/app/app.module.ts index 61f2d42..71ac5f2 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -91,6 +91,9 @@ 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"; +import { TaskWeeklySeriesCreatorComponent } from './tasks/task-weekly-series-creator/task-weekly-series-creator.component'; @NgModule({ declarations: [ AppComponent, @@ -138,6 +141,8 @@ import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy- StopScheduleManuallyComponent, ConnectionSettingsComponent, NtfySettingsComponent, + TaskSeriesCreatorComponent, + TaskWeeklySeriesCreatorComponent, ], imports: [ BrowserModule, @@ -181,6 +186,7 @@ import { NtfySettingsComponent } from './user-settings/connection-settings/ntfy- NgApexchartsModule, MatButtonToggleModule, MatGridListModule, + MatStepperModule, ], providers: [ HttpClientModule, 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..9d04aaa 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,23 @@
+ + + + + + - + 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..49b5e14 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.filter(task => !task.hasTaskSerie)); + } + + 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-detail-overview/task-detail-overview.component.html b/frontend/src/app/tasks/task-detail-overview/task-detail-overview.component.html index d745f2b..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/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/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..e000da5 --- /dev/null +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.html @@ -0,0 +1,37 @@ +

Create Task-Series

+ + +
+ 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..ed27ba5 --- /dev/null +++ b/frontend/src/app/tasks/task-series-creator/task-series-creator.component.ts @@ -0,0 +1,52 @@ +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 { + + dailyFormGroup = this._formBuilder.group({ + offsetCtrl: ['', Validators.required], + deadlineStrategyCtrl: ['', Validators.required] + }) + + endDateFormGroup = this._formBuilder.group({ + endDateCtrl: ['', Validators.required] + }) + + constructor(private _formBuilder: FormBuilder, + private taskSeriesService: TaskseriesService, + @Inject(MAT_DIALOG_DATA) public task: TaskEntityInfo, + private dialogRef: MatDialogRef) { + + } + + save() { + this.taskSeriesService.tasksTaskIDTaskseriesDailyPost(this.task.taskID,{ + offset: Number(this.dailyFormGroup.get('offsetCtrl')!.value!), + deadlineStrategy: this.convertDeadlineStrategyCtrlToDeadlineEnum(), + endingDate: moment( this.endDateFormGroup.get('endDateCtrl')!.value!).format('YYYY-MM-DDTHH:mm:ss.SSSZ') + }).subscribe({ + next: resp => { + 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/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); + } +} 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!); } }) diff --git a/openapi.yaml b/openapi.yaml index 2001027..2ff5ea4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2073,6 +2073,84 @@ 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' + /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: @@ -2336,6 +2414,7 @@ components: - finishable - hasActiveSchedules - hasPlannedSchedules + - hasTaskSerie additionalProperties: false properties: taskID: @@ -2380,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 @@ -2779,4 +2862,63 @@ 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 + - endingDate + additionalProperties: false + properties: + offset: + type: number + description: number repeating days + example: 7 + minimum: 1 + deadlineStrategy: + type: string + enum: + - DEADLINE_EQUAL_START + - DEADLINE_FIT_START + endingDate: + type: string + format: date + 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
+ + + + + + Name @@ -45,7 +62,9 @@ + +