issue-47 #60
@ -4,10 +4,8 @@
 | 
			
		||||
    <option name="autoReloadType" value="SELECTIVE" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ChangeListManager">
 | 
			
		||||
    <list default="true" id="3a869f59-290a-4ab2-b036-a878ce801bc4" name="Changes" comment="Fix wrong date">
 | 
			
		||||
    <list default="true" id="3a869f59-290a-4ab2-b036-a878ce801bc4" name="Changes" comment="Edit and Delete in Draggable Scheduler">
 | 
			
		||||
      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
 | 
			
		||||
      <change beforePath="$PROJECT_DIR$/src/main/java/core/entities/timemanager/AdvancedTaskSchedule.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/core/entities/timemanager/AdvancedTaskSchedule.java" afterDir="false" />
 | 
			
		||||
      <change beforePath="$PROJECT_DIR$/src/main/java/core/services/TaskScheduleService.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/core/services/TaskScheduleService.java" afterDir="false" />
 | 
			
		||||
    </list>
 | 
			
		||||
    <option name="SHOW_DIALOG" value="false" />
 | 
			
		||||
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
 | 
			
		||||
@ -26,7 +24,7 @@
 | 
			
		||||
  <component name="Git.Settings">
 | 
			
		||||
    <option name="RECENT_BRANCH_BY_REPOSITORY">
 | 
			
		||||
      <map>
 | 
			
		||||
        <entry key="$PROJECT_DIR$/.." value="test-failing" />
 | 
			
		||||
        <entry key="$PROJECT_DIR$/.." value="docker-deployment" />
 | 
			
		||||
      </map>
 | 
			
		||||
    </option>
 | 
			
		||||
    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
 | 
			
		||||
@ -34,6 +32,9 @@
 | 
			
		||||
  <component name="JpbToolWindowState">
 | 
			
		||||
    <option name="isToolWindowVisible" value="false" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="MavenRunner">
 | 
			
		||||
    <option name="skipTests" value="true" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ProjectColorInfo">{
 | 
			
		||||
  "customColor": "",
 | 
			
		||||
  "associatedIndex": 6
 | 
			
		||||
@ -548,18 +549,53 @@
 | 
			
		||||
      <option name="project" value="LOCAL" />
 | 
			
		||||
      <updated>1699786723938</updated>
 | 
			
		||||
    </task>
 | 
			
		||||
    <option name="localTasksCounter" value="42" />
 | 
			
		||||
    <task id="LOCAL-00042" summary="Deploy on docker (not productive yet!)">
 | 
			
		||||
      <option name="closed" value="true" />
 | 
			
		||||
      <created>1699803821051</created>
 | 
			
		||||
      <option name="number" value="00042" />
 | 
			
		||||
      <option name="presentableId" value="LOCAL-00042" />
 | 
			
		||||
      <option name="project" value="LOCAL" />
 | 
			
		||||
      <updated>1699803821051</updated>
 | 
			
		||||
    </task>
 | 
			
		||||
    <task id="LOCAL-00043" summary="Fix deleting tasks">
 | 
			
		||||
      <option name="closed" value="true" />
 | 
			
		||||
      <created>1699805283751</created>
 | 
			
		||||
      <option name="number" value="00043" />
 | 
			
		||||
      <option name="presentableId" value="LOCAL-00043" />
 | 
			
		||||
      <option name="project" value="LOCAL" />
 | 
			
		||||
      <updated>1699805283751</updated>
 | 
			
		||||
    </task>
 | 
			
		||||
    <task id="LOCAL-00044" summary="Fix parsing datetime">
 | 
			
		||||
      <option name="closed" value="true" />
 | 
			
		||||
      <created>1699806194258</created>
 | 
			
		||||
      <option name="number" value="00044" />
 | 
			
		||||
      <option name="presentableId" value="LOCAL-00044" />
 | 
			
		||||
      <option name="project" value="LOCAL" />
 | 
			
		||||
      <updated>1699806194258</updated>
 | 
			
		||||
    </task>
 | 
			
		||||
    <task id="LOCAL-00045" summary="Fix parsing datetime (diesmal hoffentlich wirklich)">
 | 
			
		||||
      <option name="closed" value="true" />
 | 
			
		||||
      <created>1699809089060</created>
 | 
			
		||||
      <option name="number" value="00045" />
 | 
			
		||||
      <option name="presentableId" value="LOCAL-00045" />
 | 
			
		||||
      <option name="project" value="LOCAL" />
 | 
			
		||||
      <updated>1699809089060</updated>
 | 
			
		||||
    </task>
 | 
			
		||||
    <task id="LOCAL-00046" summary="Edit and Delete in Draggable Scheduler">
 | 
			
		||||
      <option name="closed" value="true" />
 | 
			
		||||
      <created>1699824923378</created>
 | 
			
		||||
      <option name="number" value="00046" />
 | 
			
		||||
      <option name="presentableId" value="LOCAL-00046" />
 | 
			
		||||
      <option name="project" value="LOCAL" />
 | 
			
		||||
      <updated>1699824923378</updated>
 | 
			
		||||
    </task>
 | 
			
		||||
    <option name="localTasksCounter" value="47" />
 | 
			
		||||
    <servers />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="TypeScriptGeneratedFilesManager">
 | 
			
		||||
    <option name="version" value="3" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="VcsManagerConfiguration">
 | 
			
		||||
    <MESSAGE value="List missed Schedules" />
 | 
			
		||||
    <MESSAGE value="Forget single schedule" />
 | 
			
		||||
    <MESSAGE value="Fix marking finished task as overdue" />
 | 
			
		||||
    <MESSAGE value="Write simple Testcase for ci/cd" />
 | 
			
		||||
    <MESSAGE value="Deactivate Overall System test (temporarly)" />
 | 
			
		||||
    <MESSAGE value="Fix failing test case" />
 | 
			
		||||
    <MESSAGE value="TaskgroupRepsitoryTest" />
 | 
			
		||||
    <MESSAGE value="TaskgroupRepsitoryTest (+Delete)" />
 | 
			
		||||
@ -580,7 +616,12 @@
 | 
			
		||||
    <MESSAGE value="Update values of datetime picker automatically for editing" />
 | 
			
		||||
    <MESSAGE value="Adapt datetime-picker" />
 | 
			
		||||
    <MESSAGE value="Fix wrong date" />
 | 
			
		||||
    <option name="LAST_COMMIT_MESSAGE" value="Fix wrong date" />
 | 
			
		||||
    <MESSAGE value="Deploy on docker (not productive yet!)" />
 | 
			
		||||
    <MESSAGE value="Fix deleting tasks" />
 | 
			
		||||
    <MESSAGE value="Fix parsing datetime" />
 | 
			
		||||
    <MESSAGE value="Fix parsing datetime (diesmal hoffentlich wirklich)" />
 | 
			
		||||
    <MESSAGE value="Edit and Delete in Draggable Scheduler" />
 | 
			
		||||
    <option name="LAST_COMMIT_MESSAGE" value="Edit and Delete in Draggable Scheduler" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="XDebuggerManager">
 | 
			
		||||
    <breakpoint-manager>
 | 
			
		||||
@ -592,13 +633,18 @@
 | 
			
		||||
        </line-breakpoint>
 | 
			
		||||
        <line-breakpoint enabled="true" type="java-line">
 | 
			
		||||
          <url>file://$PROJECT_DIR$/src/main/java/core/api/controller/ScheduleController.java</url>
 | 
			
		||||
          <line>68</line>
 | 
			
		||||
          <option name="timeStamp" value="14" />
 | 
			
		||||
          <line>98</line>
 | 
			
		||||
          <option name="timeStamp" value="16" />
 | 
			
		||||
        </line-breakpoint>
 | 
			
		||||
        <line-breakpoint enabled="true" type="java-line">
 | 
			
		||||
          <url>file://$PROJECT_DIR$/src/main/java/core/services/TaskService.java</url>
 | 
			
		||||
          <line>93</line>
 | 
			
		||||
          <option name="timeStamp" value="35" />
 | 
			
		||||
        </line-breakpoint>
 | 
			
		||||
        <line-breakpoint enabled="true" type="java-line">
 | 
			
		||||
          <url>file://$PROJECT_DIR$/src/main/java/core/api/controller/ScheduleController.java</url>
 | 
			
		||||
          <line>93</line>
 | 
			
		||||
          <option name="timeStamp" value="16" />
 | 
			
		||||
          <line>83</line>
 | 
			
		||||
          <option name="timeStamp" value="36" />
 | 
			
		||||
        </line-breakpoint>
 | 
			
		||||
      </breakpoints>
 | 
			
		||||
    </breakpoint-manager>
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,15 @@
 | 
			
		||||
FROM maven:3.8.1-openjdk-17-slim AS build
 | 
			
		||||
COPY src /home/app/src
 | 
			
		||||
COPY pom.xml /home/app
 | 
			
		||||
RUN mvn -f /home/app/pom.xml clean test package
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FROM openjdk:17-jdk-alpine
 | 
			
		||||
RUN addgroup -S spring && adduser -S spring -G spring
 | 
			
		||||
USER spring:spring
 | 
			
		||||
 | 
			
		||||
ARG JAR_FILE=target/*.jar
 | 
			
		||||
COPY ${JAR_FILE} app.jar
 | 
			
		||||
 | 
			
		||||
#ARG JAR_FILE=target/*.jar
 | 
			
		||||
#COPY ${JAR_FILE} app.jar
 | 
			
		||||
COPY --from=build /home/app/target/*.jar app.jar
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
ENTRYPOINT ["java","-jar","/app.jar"]
 | 
			
		||||
@ -78,10 +78,15 @@ public class ScheduleController {
 | 
			
		||||
            return ResponseEntity.ok(scheduleResult.getResult().toScheduleInfo());
 | 
			
		||||
        } else if(scheduleFieldInfo instanceof AdvancedScheduleFieldInfo) {
 | 
			
		||||
            ServiceResult<AbstractSchedule> scheduleResult = taskScheduleService.scheduleAdvanced(permissionResult.getResult(), (AdvancedScheduleFieldInfo) scheduleFieldInfo);
 | 
			
		||||
            if(scheduleResult.getResult() != null) {
 | 
			
		||||
                return ResponseEntity.ok(scheduleResult.getResult().toScheduleInfo());
 | 
			
		||||
            } else {
 | 
			
		||||
                return ResponseEntity.status(400).body(new SimpleStatusResponse("failed"));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            return ResponseEntity.status(400).body(new SimpleStatusResponse("failed"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @PostMapping("/schedules/{scheduleID}/basic")
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package core.api.models.timemanager.taskSchedule.scheduleInfos;
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonFormat;
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
			
		||||
import org.springframework.format.annotation.DateTimeFormat;
 | 
			
		||||
 | 
			
		||||
import javax.validation.constraints.NotNull;
 | 
			
		||||
import java.time.LocalDateTime;
 | 
			
		||||
@ -10,9 +11,11 @@ public class AdvancedScheduleFieldInfo extends ScheduleFieldInfo {
 | 
			
		||||
 | 
			
		||||
    @NotNull
 | 
			
		||||
    @JsonProperty
 | 
			
		||||
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
 | 
			
		||||
    private LocalDateTime scheduleStartTime;
 | 
			
		||||
    @NotNull
 | 
			
		||||
    @JsonProperty
 | 
			
		||||
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
 | 
			
		||||
    private LocalDateTime scheduleStopTime;
 | 
			
		||||
 | 
			
		||||
    public AdvancedScheduleFieldInfo(LocalDateTime scheduleStartTime, LocalDateTime scheduleStopTime) {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import java.time.LocalDate;
 | 
			
		||||
public class BasicScheduleFieldInfo extends ScheduleFieldInfo{
 | 
			
		||||
 | 
			
		||||
    @NotNull
 | 
			
		||||
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
 | 
			
		||||
    private LocalDate scheduleDate;
 | 
			
		||||
 | 
			
		||||
    public BasicScheduleFieldInfo(LocalDate localDate) {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
package core.api.models.timemanager.tasks;
 | 
			
		||||
 | 
			
		||||
import core.api.models.timemanager.taskgroup.TaskgroupEntityInfo;
 | 
			
		||||
import core.entities.timemanager.Task;
 | 
			
		||||
import core.entities.timemanager.Taskgroup;
 | 
			
		||||
 | 
			
		||||
import java.time.LocalDate;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class TaskOverviewInfo {
 | 
			
		||||
 | 
			
		||||
@ -13,6 +16,7 @@ public class TaskOverviewInfo {
 | 
			
		||||
    private LocalDate limit;
 | 
			
		||||
    private boolean overdue;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public TaskOverviewInfo(Task task) {
 | 
			
		||||
        this.taskID = task.getTaskID();
 | 
			
		||||
        this.taskName = task.getTaskName();
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,6 @@ import java.util.List;
 | 
			
		||||
@Repository
 | 
			
		||||
public interface TaskgroupRepository extends CrudRepository<Taskgroup, Long> {
 | 
			
		||||
 | 
			
		||||
    boolean existsByTaskgroupNameAndUser(String name, User user);
 | 
			
		||||
 | 
			
		||||
    @Query("SELECT tg FROM Taskgroup tg WHERE tg.user.username = ?1")
 | 
			
		||||
    List<Taskgroup> findAllByUser(String username);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -96,6 +96,9 @@ public class TaskScheduleService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deleteSchedule(AbstractSchedule schedule) {
 | 
			
		||||
        schedule.getTask().getBasicTaskSchedules().remove(schedule);
 | 
			
		||||
        schedule.setTask(null);
 | 
			
		||||
        scheduleRepository.save(schedule);
 | 
			
		||||
        scheduleRepository.delete(schedule);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -77,11 +77,6 @@ public class TaskService {
 | 
			
		||||
            return new ServiceResult<>(ServiceExitCode.INVALID_PARAMETER);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Check for invalid date (deadline before start
 | 
			
		||||
        if(taskFieldInfo.getStartDate() != null && taskFieldInfo.getDeadline() != null &&
 | 
			
		||||
                taskFieldInfo.getDeadline().isBefore(taskFieldInfo.getStartDate())) {
 | 
			
		||||
            return new ServiceResult<>(ServiceExitCode.INVALID_PARAMETER);
 | 
			
		||||
        }
 | 
			
		||||
        task.setEta(taskFieldInfo.getEta());
 | 
			
		||||
        task.setStartDate(taskFieldInfo.getStartDate());
 | 
			
		||||
        task.setDeadline(taskFieldInfo.getDeadline());
 | 
			
		||||
@ -91,7 +86,12 @@ public class TaskService {
 | 
			
		||||
 | 
			
		||||
    public void deleteTask(Task task) {
 | 
			
		||||
        //taskScheduleService.deleteScheduleByTask(task);
 | 
			
		||||
        taskRepository.deleteByTaskID(task.getTaskID());
 | 
			
		||||
        System.err.println(task.getTaskID());
 | 
			
		||||
        task.getTaskgroup().getTasks().remove(task);
 | 
			
		||||
        taskgroupRepository.save(task.getTaskgroup());
 | 
			
		||||
        task.setTaskgroup(null);
 | 
			
		||||
        taskRepository.save(task);
 | 
			
		||||
        taskRepository.delete(task);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void clearTasks(Taskgroup taskgroup) {
 | 
			
		||||
 | 
			
		||||
@ -39,32 +39,34 @@ public class TaskgroupService {
 | 
			
		||||
            throw new NoSuchElementException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if(!taskgroupRepository.existsByTaskgroupNameAndUser(taskData.getName(), user.get())) {
 | 
			
		||||
        Taskgroup taskgroup;
 | 
			
		||||
        if(taskData.getParentID() < 0) {
 | 
			
		||||
            if(existTaskgroupOnTaskgroupLevel(null, taskData.getName(), username)) {
 | 
			
		||||
                return new ServiceResult<>(ServiceExitCode.ENTITY_ALREADY_EXIST);
 | 
			
		||||
            } else {
 | 
			
		||||
                taskgroup = new Taskgroup(taskData.getName(), user.get());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else{
 | 
			
		||||
            Optional<Taskgroup> parentTaskgroup = taskgroupRepository.findById(taskData.getParentID());
 | 
			
		||||
            if(parentTaskgroup.isEmpty()) {
 | 
			
		||||
                return new ServiceResult<>(ServiceExitCode.MISSING_ENTITY);
 | 
			
		||||
            } else if(existTaskgroupOnTaskgroupLevel(parentTaskgroup.get(), taskData.getName(), username)) {
 | 
			
		||||
                return new ServiceResult<>(ServiceExitCode.ENTITY_ALREADY_EXIST);
 | 
			
		||||
            } else {
 | 
			
		||||
                taskgroup = new Taskgroup(taskData.getName(), user.get());
 | 
			
		||||
                taskgroup.setParent(parentTaskgroup.get());
 | 
			
		||||
                parentTaskgroup.get().getChildren().add(taskgroup);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        taskgroupRepository.save(taskgroup);
 | 
			
		||||
        return new ServiceResult<>(taskgroup);
 | 
			
		||||
        } else {
 | 
			
		||||
            return new ServiceResult<>(ServiceExitCode.ENTITY_ALREADY_EXIST);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ServiceExitCode editTaskgroup(Taskgroup taskgroup, TaskgroupFieldInfo taskgroupFieldInfo) {
 | 
			
		||||
        if(!taskgroup.getTaskgroupName().equals(taskgroupFieldInfo.getName())) {
 | 
			
		||||
            //Check if another taskgroup with the new name is already existent for the user of the taskgroup
 | 
			
		||||
            if(!taskgroupRepository.existsByTaskgroupNameAndUser(taskgroupFieldInfo.getName(), taskgroup.getUser())) {
 | 
			
		||||
            if(!existTaskgroupOnTaskgroupLevel(taskgroup.getParent(), taskgroupFieldInfo.getName(), taskgroup.getUser().getUsername())) {
 | 
			
		||||
                taskgroup.setTaskgroupName(taskgroupFieldInfo.getName());
 | 
			
		||||
                taskgroupRepository.save(taskgroup);
 | 
			
		||||
                return ServiceExitCode.OK;
 | 
			
		||||
@ -76,6 +78,24 @@ public class TaskgroupService {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean existTaskgroupOnTaskgroupLevel(Taskgroup taskgroup, String taskgroupName, String username) {
 | 
			
		||||
        if(taskgroup == null) {
 | 
			
		||||
            List<Taskgroup> topTaskgroups = getTopTaskgroupsByUser(username);
 | 
			
		||||
            for(Taskgroup taskgroup1 : topTaskgroups) {
 | 
			
		||||
                if(taskgroup1.getTaskgroupName().equals(taskgroupName)) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            for(Taskgroup sibling : taskgroup.getChildren()) {
 | 
			
		||||
                if(sibling.getTaskgroupName().equals(taskgroupName)) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void deleteTaskgroup(Taskgroup taskgroup) {
 | 
			
		||||
        taskgroupRepository.delete(taskgroup);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ spring.jpa.open-in-view=false
 | 
			
		||||
spring.data.rest.base-path=/api
 | 
			
		||||
 | 
			
		||||
# Spring Server setup
 | 
			
		||||
server.address=127.0.0.1
 | 
			
		||||
server.address=0.0.0.0
 | 
			
		||||
server.port=8080
 | 
			
		||||
server.compression.enabled=true
 | 
			
		||||
server.http2.enabled=true
 | 
			
		||||
@ -42,3 +42,4 @@ spring.servlet.multipart.max-request-size=4196KB
 | 
			
		||||
demo.webapp.jwtSecret=demoWebappSecretKey
 | 
			
		||||
demo.webapp.jwtExpirationMS=86400000
 | 
			
		||||
spring.jackson.time-zone=UTC
 | 
			
		||||
server.servlet.session.cookie.samesite=None
 | 
			
		||||
@ -37,25 +37,6 @@ public class TaskgroupRepsitoryTest {
 | 
			
		||||
        assertEquals(8, result_user1.size());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Sql("classpath:taskgroupRepositoryTestEntries.sql")
 | 
			
		||||
    void existsByTaskgroupNameAndUser() {
 | 
			
		||||
        User testUser1 = testEntityManager.find(User.class, 1L);
 | 
			
		||||
        User testUser2 = testEntityManager.find(User.class, 2L);
 | 
			
		||||
 | 
			
		||||
        //Situation 1: Taskgroup exists but within another user
 | 
			
		||||
        assertFalse(taskgroupRepository.existsByTaskgroupNameAndUser("No children", testUser2));
 | 
			
		||||
 | 
			
		||||
        //Situation 2: Taskgroup exists not
 | 
			
		||||
        assertFalse(taskgroupRepository.existsByTaskgroupNameAndUser("ada", testUser1));
 | 
			
		||||
 | 
			
		||||
        //Situation 3: Taskgroup (top) exists on user
 | 
			
		||||
        assertTrue(taskgroupRepository.existsByTaskgroupNameAndUser("No children", testUser1));
 | 
			
		||||
 | 
			
		||||
        //Situation 4: Taskgroup exists on user but not on top level
 | 
			
		||||
        assertTrue(taskgroupRepository.existsByTaskgroupNameAndUser("Taskgroup 1.1", testUser1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    @Sql("classpath:taskgroupRepositoryTestEntries.sql")
 | 
			
		||||
    void findAllTopTaskgroupsByUser() {
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ services:
 | 
			
		||||
 | 
			
		||||
  db:
 | 
			
		||||
    image: mariadb:latest
 | 
			
		||||
    container_name: template_mysql
 | 
			
		||||
    container_name: timemanager-db
 | 
			
		||||
    environment:
 | 
			
		||||
      MYSQL_ROOT_PASSWORD: edkvcjReDxJ9Z8hq
 | 
			
		||||
  backend:
 | 
			
		||||
@ -14,6 +14,7 @@ services:
 | 
			
		||||
    links:
 | 
			
		||||
      - "db:app_db"
 | 
			
		||||
    environment:
 | 
			
		||||
      SERVER_ADDRESS: 0.0.0.0
 | 
			
		||||
      SPRING_DATASOURCE_USERNAME: root
 | 
			
		||||
      SPRING_DATASOURCE_PASSWORD: edkvcjReDxJ9Z8hq
 | 
			
		||||
      SPRING_DATASOURCE_URL: jdbc:mariadb://app_db:3306/restservice?createDatabaseIfNotExist=true&autoReconnect=true
 | 
			
		||||
@ -27,15 +28,9 @@ services:
 | 
			
		||||
    restart: always
 | 
			
		||||
    links:
 | 
			
		||||
      - backend:backend
 | 
			
		||||
    depends_on:
 | 
			
		||||
      - "backend"
 | 
			
		||||
    ports:
 | 
			
		||||
      - "4200:80"
 | 
			
		||||
    environment:
 | 
			
		||||
      # change this env-variable in prod
 | 
			
		||||
      BACKEND_URL: http://backend/
 | 
			
		||||
    # alternative way manipulate the api.json:
 | 
			
		||||
    #{
 | 
			
		||||
    #  "api": "http://backend/"
 | 
			
		||||
    #}
 | 
			
		||||
    # bind-mount it read-only via:
 | 
			
		||||
    #volumes:
 | 
			
		||||
    #  - ./api.json:/usr/share/nginx/html/assets/api.json:ro
 | 
			
		||||
      BACKEND_URL: http://backend:8080
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,23 @@
 | 
			
		||||
FROM node:16-alpine as builder
 | 
			
		||||
 | 
			
		||||
### STAGE 1: Build ###
 | 
			
		||||
FROM node:18.17.1 AS build
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
COPY package*.json ./
 | 
			
		||||
 | 
			
		||||
RUN npm set progress=false && npm config set depth 0 && npm cache clean --force
 | 
			
		||||
 | 
			
		||||
RUN npm i && mkdir /ng-app && cp -R ./node_modules ./ng-app
 | 
			
		||||
 | 
			
		||||
WORKDIR /ng-app
 | 
			
		||||
 | 
			
		||||
RUN npm install
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN npm run build --prod
 | 
			
		||||
 | 
			
		||||
RUN $(npm bin)/ng build --configuration=production --build-optimizer
 | 
			
		||||
### STAGE 2: Deploy ###
 | 
			
		||||
FROM nginx:latest
 | 
			
		||||
RUN apt-get update
 | 
			
		||||
RUN apt-get upgrade -y
 | 
			
		||||
RUN apt-get install iputils-ping -y
 | 
			
		||||
COPY --from=build /app/dist/frontend /usr/share/nginx/html
 | 
			
		||||
COPY nginx.conf.template /etc/nginx/nginx.conf.template
 | 
			
		||||
COPY docker-entrypoint.sh /app-entrypoint.sh
 | 
			
		||||
 | 
			
		||||
RUN chmod +x /app-entrypoint.sh
 | 
			
		||||
 | 
			
		||||
FROM nginx:stable-alpine
 | 
			
		||||
ENTRYPOINT [ "/app-entrypoint.sh" ]
 | 
			
		||||
 | 
			
		||||
EXPOSE 80
 | 
			
		||||
 | 
			
		||||
COPY nginx/nginx.conf.template /
 | 
			
		||||
 | 
			
		||||
RUN rm -rf /usr/share/nginx/html/*
 | 
			
		||||
 | 
			
		||||
COPY --from=builder /ng-app/dist/frontend/ /usr/share/nginx/html
 | 
			
		||||
RUN chown -R nginx:nginx /usr/share/nginx/html/
 | 
			
		||||
 | 
			
		||||
COPY nginx/run_nginx.sh /
 | 
			
		||||
 | 
			
		||||
CMD ["sh", "/run_nginx.sh"]
 | 
			
		||||
CMD ["nginx", "-g", "daemon off;"]
 | 
			
		||||
 | 
			
		||||
@ -38,8 +38,8 @@
 | 
			
		||||
              "budgets": [
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "initial",
 | 
			
		||||
                  "maximumWarning": "500kb",
 | 
			
		||||
                  "maximumError": "1mb"
 | 
			
		||||
                  "maximumWarning": "2mb",
 | 
			
		||||
                  "maximumError": "5mb"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "anyComponentStyle",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/docker-entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/docker-entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
cd /etc/nginx/
 | 
			
		||||
envsubst '$BACKEND_URL' < nginx.conf.template > nginx.conf
 | 
			
		||||
exec "$@"
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
user  nginx;
 | 
			
		||||
worker_processes  1;
 | 
			
		||||
 | 
			
		||||
error_log  /var/log/nginx/error.log warn;
 | 
			
		||||
pid        /var/run/nginx.pid;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,12 +13,6 @@ http {
 | 
			
		||||
    include       /etc/nginx/mime.types;
 | 
			
		||||
    default_type  application/octet-stream;
 | 
			
		||||
 | 
			
		||||
    log_format  main  '${DOLLAR}remote_addr - ${DOLLAR}remote_user [${DOLLAR}time_local] "${DOLLAR}request" '
 | 
			
		||||
                      '${DOLLAR}status ${DOLLAR}body_bytes_sent "${DOLLAR}http_referer" '
 | 
			
		||||
                      '"${DOLLAR}http_user_agent" "${DOLLAR}http_x_forwarded_for"';
 | 
			
		||||
 | 
			
		||||
    access_log  /var/log/nginx/access.log  main;
 | 
			
		||||
 | 
			
		||||
    sendfile        on;
 | 
			
		||||
    #tcp_nopush     on;
 | 
			
		||||
 | 
			
		||||
@ -40,12 +33,17 @@ http {
 | 
			
		||||
         add_header "Access-Control-Allow-Methods" 'GET, POST, OPTIONS, PUT, DELETE';
 | 
			
		||||
         add_header "Access-Control-Allow-Headers" 'X-Requested-With,Accept,Content-Type, Origin';
 | 
			
		||||
 | 
			
		||||
        location / {
 | 
			
		||||
                try_files ${DOLLAR}uri ${DOLLAR}uri/ /index.html;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        location /api {
 | 
			
		||||
           proxy_pass ${BACKEND_URL};
 | 
			
		||||
        }
 | 
			
		||||
           add_header "Access-Control-Allow-Origin" "*";
 | 
			
		||||
           add_header "Access-Control-Allow-Methods" 'GET, POST, OPTIONS, PUT, DELETE';
 | 
			
		||||
           add_header "Access-Control-Allow-Headers" 'X-Requested-With,Accept,Content-Type, Origin';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        location / {
 | 
			
		||||
                try_files $uri $uri/ /index.html =404;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
# generate nginx-configuration (insert env variables)
 | 
			
		||||
 | 
			
		||||
export DOLLAR='$'
 | 
			
		||||
envsubst < ./nginx.conf.template > /etc/nginx/nginx.conf
 | 
			
		||||
 | 
			
		||||
# start nginx
 | 
			
		||||
exec nginx -g "daemon off;"
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/proxy.conf.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/proxy.conf.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "/api": {
 | 
			
		||||
    "target": "http://backend",
 | 
			
		||||
    "secure": false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -35,7 +35,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class AccountService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class HistoryService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class LoginService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class PropertiesService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class ScheduleService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class TaskService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class TaskgroupService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ import { Configuration }                                     from '../configurat
 | 
			
		||||
})
 | 
			
		||||
export class UsersService {
 | 
			
		||||
 | 
			
		||||
    protected basePath = 'http://localhost:8080/api';
 | 
			
		||||
    protected basePath = 'http://127.0.0.1:8080/api';
 | 
			
		||||
    public defaultHeaders = new HttpHeaders();
 | 
			
		||||
    public configuration = new Configuration();
 | 
			
		||||
    public encoder: HttpParameterCodec;
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@
 | 
			
		||||
 * https://openapi-generator.tech
 | 
			
		||||
 * Do not edit the class manually.
 | 
			
		||||
 */
 | 
			
		||||
import { TaskgroupEntityInfo } from './taskgroupEntityInfo';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface TaskOverviewInfo {
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@ import {MissedSchedulesComponent} from "./missed-schedules/missed-schedules.comp
 | 
			
		||||
import {OverdueTaskOverviewComponent} from "./overdue-task-overview/overdue-task-overview.component";
 | 
			
		||||
import {UpcomingTaskOverviewComponent} from "./upcoming-task-overview/upcoming-task-overview.component";
 | 
			
		||||
import {ActiveTaskOverviewComponent} from "./active-task-overview/active-task-overview.component";
 | 
			
		||||
import {DraggableSchedulerComponent} from "./schedules/draggable-scheduler/draggable-scheduler.component";
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {path: '', component: MainComponent},
 | 
			
		||||
@ -24,7 +25,8 @@ const routes: Routes = [
 | 
			
		||||
  {path: 'reschedule', component: MissedSchedulesComponent},
 | 
			
		||||
  {path: 'overdue', component: OverdueTaskOverviewComponent},
 | 
			
		||||
  {path: 'upcoming', component: UpcomingTaskOverviewComponent},
 | 
			
		||||
  {path: 'active', component: ActiveTaskOverviewComponent}
 | 
			
		||||
  {path: 'active', component: ActiveTaskOverviewComponent},
 | 
			
		||||
  {path: 'scheduler', component: DraggableSchedulerComponent}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@
 | 
			
		||||
  <button mat-button aria-label="Organize" *ngIf="authService.hasKey" [matMenuTriggerFor]="organizeMenu">Organize ▾</button>
 | 
			
		||||
  <mat-menu #organizeMenu=matMenu>
 | 
			
		||||
    <button mat-menu-item routerLink="taskgroups/" aria-label="Task groups">Taskgroups</button>
 | 
			
		||||
    <button mat-menu-item routerLink="scheduler/" aria-label="Task groups">Scheduler</button>
 | 
			
		||||
    <button mat-menu-item routerLink="active/" aria-label="Missed Schedules">Active Tasks</button>
 | 
			
		||||
    <button mat-menu-item routerLink="upcoming/" aria-label="Upcoming Tasks">Upcoming Tasks</button>
 | 
			
		||||
    <button mat-menu-item routerLink="overdue/" aria-label="Overdue Tasks">Overdue Tasks</button>
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import { AppComponent } from './app.component';
 | 
			
		||||
import {LoginComponent} from "./auth/login/login.component";
 | 
			
		||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
 | 
			
		||||
import {HttpClient, HttpClientModule, HttpHandler} from "@angular/common/http";
 | 
			
		||||
import {ApiModule, Configuration} from "../api";
 | 
			
		||||
import {ApiModule, Configuration, ConfigurationParameters} from "../api";
 | 
			
		||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 | 
			
		||||
import {MatToolbarModule} from "@angular/material/toolbar";
 | 
			
		||||
import {MatIconModule} from "@angular/material/icon";
 | 
			
		||||
@ -79,6 +79,7 @@ import {NgxMaterialTimepickerModule} from "ngx-material-timepicker";
 | 
			
		||||
import { DateTimePickerComponent } from './date-time-picker/date-time-picker.component';
 | 
			
		||||
import {MatSliderModule} from "@angular/material/slider";
 | 
			
		||||
import {MatLegacySliderModule} from "@angular/material/legacy-slider";
 | 
			
		||||
import { DraggableSchedulerComponent } from './schedules/draggable-scheduler/draggable-scheduler.component';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    AppComponent,
 | 
			
		||||
@ -117,7 +118,8 @@ import {MatLegacySliderModule} from "@angular/material/legacy-slider";
 | 
			
		||||
    UpcomingTaskOverviewComponent,
 | 
			
		||||
    ActiveTaskOverviewComponent,
 | 
			
		||||
    AdvancedSchedulerComponent,
 | 
			
		||||
    DateTimePickerComponent
 | 
			
		||||
    DateTimePickerComponent,
 | 
			
		||||
    DraggableSchedulerComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    BrowserModule,
 | 
			
		||||
@ -178,7 +180,7 @@ import {MatLegacySliderModule} from "@angular/material/legacy-slider";
 | 
			
		||||
            // context
 | 
			
		||||
            "API_TOKEN": authService.getAccessToken.bind(authService)
 | 
			
		||||
          },
 | 
			
		||||
          //basePath: environment.api
 | 
			
		||||
          basePath: environment.api,
 | 
			
		||||
        }
 | 
			
		||||
      ),
 | 
			
		||||
      deps: [AuthService],
 | 
			
		||||
 | 
			
		||||
@ -4,9 +4,9 @@
 | 
			
		||||
    <mat-card-content>
 | 
			
		||||
      <h3>
 | 
			
		||||
        <span *ngFor="let taskgroup of task.taskgroups">
 | 
			
		||||
          <a class="undecorated-link">{{taskgroup.taskgroupName}}</a>/
 | 
			
		||||
          <a class="undecorated-link" [routerLink]="['/taskgroups', taskgroup.taskgroupID]">{{taskgroup.taskgroupName}}</a>/
 | 
			
		||||
        </span>
 | 
			
		||||
        <a class="undecorated-link">{{task.taskName}}</a>
 | 
			
		||||
        <a class="undecorated-link" [routerLink]="['/taskgroups', task.taskgroups[task.taskgroups.length-1].taskgroupID, 'tasks', task.taskID]">{{task.taskName}}</a>
 | 
			
		||||
      </h3>
 | 
			
		||||
      <mat-progress-bar mode="determinate" [value]="calcProgress(task)"></mat-progress-bar>
 | 
			
		||||
      <div class="originally-planned-container">
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,8 @@ export class AdvancedSchedulerComponent implements OnInit, OnChanges{
 | 
			
		||||
 | 
			
		||||
  schedule() {
 | 
			
		||||
    if(this.scheduleInfo == undefined) {
 | 
			
		||||
      console.log("Direct Date" + this.selectedStartTime!.toString())
 | 
			
		||||
      console.log("Moment Format" + moment(this.selectedStartTime!).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))
 | 
			
		||||
      this.scheduleService.schedulesTaskIDAdvancedPut(this.task!.taskID, {
 | 
			
		||||
        scheduleStartTime: moment(this.selectedStartTime!).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
 | 
			
		||||
        scheduleStopTime: moment(this.selectedStopTime!).format('YYYY-MM-DDTHH:mm:ss.SSSZ')
 | 
			
		||||
@ -141,6 +143,8 @@ export class AdvancedSchedulerComponent implements OnInit, OnChanges{
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log("Direct Date" + this.selectedStartTime!.toString())
 | 
			
		||||
      console.log("Moment Format" + moment(this.selectedStartTime!).format('YYYY-MM-DDTHH:mm:ss.SSSZ'))
 | 
			
		||||
      this.scheduleService.schedulesScheduleIDAdvancedPost(this.scheduleInfo.scheduleID!, {
 | 
			
		||||
        scheduleStartTime: moment(this.selectedStartTime!).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
 | 
			
		||||
        scheduleStopTime: moment(this.selectedStopTime!).format('YYYY-MM-DDTHH:mm:ss.SSSZ')
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,60 @@
 | 
			
		||||
.container {
 | 
			
		||||
  margin: 20px auto;
 | 
			
		||||
  width: 80%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.spacer {
 | 
			
		||||
  margin-bottom: 2.5%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 600px) {
 | 
			
		||||
  .container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    margin: 20px 10px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.schedule-header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.long-form {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.taskgroup-overview {
 | 
			
		||||
  width: 90%;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app-task-overview {
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .mat-mdc-list-base {
 | 
			
		||||
  --mdc-list-list-item-label-text-color: black
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .mat-mdc-list-base .taskgroup-btn, ::ng-deep .mat-mdc-list-base .taskgroup-last-btn  {
 | 
			
		||||
  --mdc-list-list-item-label-text-color: black
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.task-card {
 | 
			
		||||
  background-color: #f3f3f3;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  line-height: 4em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.lightBlueBtn {
 | 
			
		||||
  background-color: #3498db;
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .cal-event-title {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,54 @@
 | 
			
		||||
 | 
			
		||||
<div class="container">
 | 
			
		||||
  <app-navigation-link-list #navLinkList [navigationLinks]="defaultNavigationLinkPath"></app-navigation-link-list>
 | 
			
		||||
 | 
			
		||||
  <div class="schedule-header">
 | 
			
		||||
    <h1>Monday, {{ viewDate | calendarDate:(view + 'ViewTitle'):'en':1 }}</h1>
 | 
			
		||||
    <mat-form-field style="float:right;">
 | 
			
		||||
      <mat-label>Schedule Strategy</mat-label>
 | 
			
		||||
      <mat-select [(ngModel)]="scheduleStrategy">
 | 
			
		||||
        <mat-option [value]="1">Basic</mat-option>
 | 
			
		||||
        <!--<mat-option [value]="2">Moderate</mat-option>-->
 | 
			
		||||
        <mat-option [value]="3">Advanced</mat-option>
 | 
			
		||||
      </mat-select>
 | 
			
		||||
    </mat-form-field>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div style="display: flex; justify-content: space-between;">
 | 
			
		||||
    <div style="width: 75%">
 | 
			
		||||
      <mwl-calendar-week-view [viewDate]="viewDate" [daysInWeek]="daysInWeek" [dayStartHour]="7" [dayEndHour]="21" [refresh]="refresh"
 | 
			
		||||
                              [snapDraggedEvents]="false"
 | 
			
		||||
                              (eventTimesChanged)="eventDropped($event)" [events]="events" (eventClicked)="eventClicked('Click', $event.event)"
 | 
			
		||||
      >
 | 
			
		||||
      </mwl-calendar-week-view>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div style="width: 23%;">
 | 
			
		||||
      <div *ngIf="tasks.length > 0">
 | 
			
		||||
        <mat-action-list style="padding: 0;">
 | 
			
		||||
          <button mat-list-item class="lightBlueBtn" [routerLink]="['/scheduler']">Tasks</button>
 | 
			
		||||
        </mat-action-list>
 | 
			
		||||
        <div style="margin-bottom: 20px">
 | 
			
		||||
          <div mwlDroppable
 | 
			
		||||
               (drop)="externalDrop($event.dropData.event)"
 | 
			
		||||
               dragOverClass="drag-over">
 | 
			
		||||
            <div *ngFor="let event of tasks"  mwlDraggable
 | 
			
		||||
                 [dropData]="{event: event}"
 | 
			
		||||
                 [touchStartLongPress]="{ delay: 300, delta: 30 }"
 | 
			
		||||
                 dragActiveClass="drag-active" class="task-card">
 | 
			
		||||
              <div>
 | 
			
		||||
                <a href="javascript:;" [style.color]="event.color" style="text-decoration: none; color: black; margin-left: 20px">
 | 
			
		||||
                  {{ event.title }}
 | 
			
		||||
                </a>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
      <app-taskgroup-overview (taskgroupSelected)="onSelectTaskgroup($event)"></app-taskgroup-overview>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { DraggableSchedulerComponent } from './draggable-scheduler.component';
 | 
			
		||||
 | 
			
		||||
describe('DraggableSchedulerComponent', () => {
 | 
			
		||||
  let component: DraggableSchedulerComponent;
 | 
			
		||||
  let fixture: ComponentFixture<DraggableSchedulerComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [DraggableSchedulerComponent]
 | 
			
		||||
    });
 | 
			
		||||
    fixture = TestBed.createComponent(DraggableSchedulerComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,285 @@
 | 
			
		||||
import {Component, ViewChild} from '@angular/core';
 | 
			
		||||
import {NavigationLink, NavigationLinkListComponent} from "../../navigation-link-list/navigation-link-list.component";
 | 
			
		||||
import {
 | 
			
		||||
    AdvancedScheduleInfo,
 | 
			
		||||
    BasicScheduleInfo,
 | 
			
		||||
    ScheduleInfo,
 | 
			
		||||
    ScheduleService,
 | 
			
		||||
    TaskEntityInfo,
 | 
			
		||||
    TaskgroupEntityInfo,
 | 
			
		||||
    TaskOverviewInfo, TaskShortInfo
 | 
			
		||||
} from "../../../api";
 | 
			
		||||
import {Subject} from "rxjs";
 | 
			
		||||
import {CalendarEvent, CalendarEventAction, CalendarEventTimesChangedEvent, CalendarView} from "angular-calendar";
 | 
			
		||||
import {TaskOverviewData} from "../../dashboard/taskgroup-overview/taskgroup-overview.component";
 | 
			
		||||
import {EventColor} from "calendar-utils";
 | 
			
		||||
import * as moment from "moment";
 | 
			
		||||
import {Router} from "@angular/router";
 | 
			
		||||
 | 
			
		||||
const colors: Record<string, EventColor> = {
 | 
			
		||||
  red: {
 | 
			
		||||
    primary: '#ad2121',
 | 
			
		||||
    secondary: '#FAE3E3',
 | 
			
		||||
  },
 | 
			
		||||
  blue: {
 | 
			
		||||
    primary: '#1e90ff',
 | 
			
		||||
    secondary: '#D1E8FF',
 | 
			
		||||
  },
 | 
			
		||||
  yellow: {
 | 
			
		||||
    primary: '#e3bc08',
 | 
			
		||||
    secondary: '#FDF1BA',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-draggable-scheduler',
 | 
			
		||||
  templateUrl: './draggable-scheduler.component.html',
 | 
			
		||||
  styleUrls: ['./draggable-scheduler.component.css'],
 | 
			
		||||
  styles: [
 | 
			
		||||
        `
 | 
			
		||||
      h3 {
 | 
			
		||||
        margin: 0 0 10px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      pre {
 | 
			
		||||
        background-color: #f5f5f5;
 | 
			
		||||
        padding: 15px;
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class DraggableSchedulerComponent {
 | 
			
		||||
  defaultNavigationLinkPath: NavigationLink[] = [
 | 
			
		||||
    {
 | 
			
		||||
      linkText: "Dashboard",
 | 
			
		||||
      routerLink: ['/']
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      linkText: "Taskgroups",
 | 
			
		||||
      routerLink: ["/taskgroups"]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
  taskgroups: TaskgroupEntityInfo[] = []
 | 
			
		||||
  taskgroup: TaskgroupEntityInfo | undefined
 | 
			
		||||
  taskgroupPath: TaskgroupEntityInfo[] = []
 | 
			
		||||
  taskgroupID: number = -1;
 | 
			
		||||
  scheduleID: number = -1;
 | 
			
		||||
  editedSchedule: ScheduleInfo | undefined
 | 
			
		||||
  @ViewChild('navLinkList') navLinkListComponent: NavigationLinkListComponent | undefined
 | 
			
		||||
 | 
			
		||||
  task: TaskEntityInfo | undefined
 | 
			
		||||
  scheduleStrategy: number = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  /**************************************************/
 | 
			
		||||
  //Calendar-Stuff
 | 
			
		||||
  /**************************************************/
 | 
			
		||||
  view: CalendarView = CalendarView.Week;
 | 
			
		||||
  viewDate = new Date();
 | 
			
		||||
  daysInWeek = 7;
 | 
			
		||||
  refresh: Subject<void> = new Subject<void>()
 | 
			
		||||
  events: CalendarEvent[] = []
 | 
			
		||||
 | 
			
		||||
  actions: CalendarEventAction[] = [
 | 
			
		||||
      {
 | 
			
		||||
          label: '<i class="fas fa-fw fa-pencil-alt"></i>',
 | 
			
		||||
          a11yLabel: 'Edit',
 | 
			
		||||
          onClick: ({ event }: { event: CalendarEvent }): void => {
 | 
			
		||||
            this.eventClicked('Edit', event)
 | 
			
		||||
          },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
          label: '<i class="fas fa-fw fa-trash-alt"></i>',
 | 
			
		||||
          a11yLabel: 'Delete',
 | 
			
		||||
          onClick: ({ event }: { event: CalendarEvent }): void => {
 | 
			
		||||
              this.eventClicked('Delete', event);
 | 
			
		||||
          },
 | 
			
		||||
      },
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  tasks: CalendarEvent[] = []
 | 
			
		||||
  selectedTaskgroupID: number | undefined
 | 
			
		||||
 | 
			
		||||
  constructor(private scheduleService: ScheduleService,
 | 
			
		||||
              private router: Router) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.scheduleService.schedulesGet().subscribe({
 | 
			
		||||
        next: resp => {
 | 
			
		||||
          resp.forEach(schedule => {
 | 
			
		||||
            if(schedule.scheduleType == 'BASIC') {
 | 
			
		||||
              const basicSchedule = schedule as BasicScheduleInfo
 | 
			
		||||
                this.events.push({
 | 
			
		||||
                    title: this.computeTaskPath(schedule.taskgroupPath, schedule.task),
 | 
			
		||||
                    color: colors['red'],
 | 
			
		||||
                    start: new Date(basicSchedule.scheduleDate),
 | 
			
		||||
                    actions: this.actions,
 | 
			
		||||
                    allDay: true,
 | 
			
		||||
                    draggable: true,
 | 
			
		||||
                    resizable: {
 | 
			
		||||
                        beforeStart: true,
 | 
			
		||||
                        afterEnd: true
 | 
			
		||||
                    },
 | 
			
		||||
                    meta: {
 | 
			
		||||
                        taskID: schedule.task.taskID,
 | 
			
		||||
                        scheduleID: schedule.scheduleID,
 | 
			
		||||
                        taskgroupID: schedule.taskgroupPath[0].taskgroupID
 | 
			
		||||
                    },
 | 
			
		||||
 | 
			
		||||
                })
 | 
			
		||||
            } else {
 | 
			
		||||
                const advancedSchedule = schedule as AdvancedScheduleInfo
 | 
			
		||||
                this.events.push({
 | 
			
		||||
                    title:  this.computeTaskPath(schedule.taskgroupPath, schedule.task),
 | 
			
		||||
                    color: colors['red'],
 | 
			
		||||
                    cssClass: 'test',
 | 
			
		||||
                    start: new Date(advancedSchedule.scheduleStartTime),
 | 
			
		||||
                    end: new Date(advancedSchedule.scheduleStopTime),
 | 
			
		||||
                    actions: this.actions,
 | 
			
		||||
                    draggable: true,
 | 
			
		||||
                    resizable: {
 | 
			
		||||
                        beforeStart: true,
 | 
			
		||||
                        afterEnd: true
 | 
			
		||||
                    },
 | 
			
		||||
                    meta: {
 | 
			
		||||
                        taskID: schedule.task.taskID,
 | 
			
		||||
                        scheduleID: schedule.scheduleID,
 | 
			
		||||
                        taskgroupID: schedule.taskgroupPath[0].taskgroupID
 | 
			
		||||
                    },
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          console.log(this.events)
 | 
			
		||||
          this.refresh.next();
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSelectTaskgroup(taskOverviewData: TaskOverviewData) {
 | 
			
		||||
    this.tasks = [];
 | 
			
		||||
    taskOverviewData.tasks.forEach(task => {
 | 
			
		||||
      this.tasks.push({
 | 
			
		||||
        title: task.taskName,
 | 
			
		||||
        color: colors['yellow'],
 | 
			
		||||
        start: new Date(),
 | 
			
		||||
        draggable: true,
 | 
			
		||||
        cssClass: 'test',
 | 
			
		||||
        resizable: {
 | 
			
		||||
          beforeStart: true,
 | 
			
		||||
          afterEnd: true
 | 
			
		||||
        },
 | 
			
		||||
        meta: {
 | 
			
		||||
          taskID: task.taskID,
 | 
			
		||||
          scheduleID: undefined,
 | 
			
		||||
          taskgroupID: taskOverviewData.taskgroupID
 | 
			
		||||
        },
 | 
			
		||||
        actions: this.actions
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    this.selectedTaskgroupID = taskOverviewData.taskgroupID;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  externalDrop(event: CalendarEvent) {
 | 
			
		||||
    if (this.tasks.indexOf(event) === -1) {
 | 
			
		||||
      this.events = this.events.filter((iEvent) => iEvent !== event);
 | 
			
		||||
      this.tasks.push(event);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  eventDropped({
 | 
			
		||||
                 event,
 | 
			
		||||
                 newStart,
 | 
			
		||||
                 newEnd,
 | 
			
		||||
                 allDay,
 | 
			
		||||
               }: CalendarEventTimesChangedEvent): void {
 | 
			
		||||
    const externalIndex = this.tasks.indexOf(event);
 | 
			
		||||
    if (typeof allDay !== 'undefined') {
 | 
			
		||||
      event.allDay = allDay;
 | 
			
		||||
    }
 | 
			
		||||
    if (externalIndex > -1) {
 | 
			
		||||
      this.tasks.splice(externalIndex, 1);
 | 
			
		||||
      this.events.push(event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.start = newStart;
 | 
			
		||||
    if (newEnd) {
 | 
			
		||||
      event.end = newEnd;
 | 
			
		||||
    }
 | 
			
		||||
    this.events = [...this.events];
 | 
			
		||||
 | 
			
		||||
    if(externalIndex > -1) {
 | 
			
		||||
      //Create new schedule as a new event was dropped
 | 
			
		||||
      if(event.allDay) {
 | 
			
		||||
        this.scheduleService.schedulesTaskIDBasicPut(event.meta.taskID,{
 | 
			
		||||
          scheduleDate: moment(event.start).format('YYYY-MM-DDTHH:mm:ss.SSSZ')
 | 
			
		||||
        }).subscribe({
 | 
			
		||||
            next: resp => {
 | 
			
		||||
              event.meta.scheduleID = resp.scheduleID;
 | 
			
		||||
              event.color = colors['red'];
 | 
			
		||||
              event.title = this.computeTaskPath(resp.taskgroupPath, resp.task)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("Start: " + event.start);
 | 
			
		||||
        console.log("End: " + event.end);
 | 
			
		||||
        if(event.end == undefined) {
 | 
			
		||||
          event.end = moment(event.start).add(30, 'm').toDate()
 | 
			
		||||
        }
 | 
			
		||||
        this.scheduleService.schedulesTaskIDAdvancedPut(event.meta.taskID, {
 | 
			
		||||
            scheduleStartTime: moment(event.start).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
 | 
			
		||||
            scheduleStopTime: moment(event.end).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
 | 
			
		||||
        }).subscribe({
 | 
			
		||||
            next: resp => {
 | 
			
		||||
                event.meta.scheduleID = resp.scheduleID;
 | 
			
		||||
                event.color = colors['red'];
 | 
			
		||||
                event.title = this.computeTaskPath(resp.taskgroupPath, resp.task)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
        if(event.allDay) {
 | 
			
		||||
            this.scheduleService.schedulesScheduleIDBasicPost(event.meta.scheduleID,{
 | 
			
		||||
                scheduleDate: moment(event.start).format('YYYY-MM-DDTHH:mm:ss.SSSZ')
 | 
			
		||||
            }).subscribe({
 | 
			
		||||
                next: resp => {
 | 
			
		||||
                    console.log("Updated")
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            this.scheduleService.schedulesScheduleIDAdvancedPost(event.meta.scheduleID, {
 | 
			
		||||
                scheduleStartTime: moment(event.start).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
 | 
			
		||||
                scheduleStopTime: moment(event.end).format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
 | 
			
		||||
            }).subscribe({
 | 
			
		||||
                next: resp => {
 | 
			
		||||
                   console.log("Updated")
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  computeTaskPath(taskgroupPath: TaskgroupEntityInfo[], task: TaskShortInfo | TaskEntityInfo) {
 | 
			
		||||
        let result = "";
 | 
			
		||||
        taskgroupPath.forEach(taskgroupPathPart => {
 | 
			
		||||
            result += taskgroupPathPart.taskgroupName + "/"
 | 
			
		||||
        });
 | 
			
		||||
        result += task!.taskName
 | 
			
		||||
        return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    eventClicked(action: string, event: CalendarEvent): void {
 | 
			
		||||
        if(action == 'Click') {
 | 
			
		||||
            this.router.navigateByUrl("/taskgroups/" + event.meta.taskgroupID + "/tasks/" + event.meta.taskID);
 | 
			
		||||
        } else if(action == 'Edit') {
 | 
			
		||||
            this.router.navigateByUrl("/taskgroups/" + event.meta.taskgroupID + "/tasks/" + event.meta.taskID + "/schedule/" + event.meta.scheduleID);
 | 
			
		||||
        } else if(action == 'Delete') {
 | 
			
		||||
            this.scheduleService.schedulesScheduleIDDelete(event.meta.scheduleID).subscribe({
 | 
			
		||||
                next: resp => {
 | 
			
		||||
                    this.events = this.events.filter(calendarEvent => calendarEvent.meta.scheduleID !== event.meta.scheduleID);
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -26,3 +26,6 @@
 | 
			
		||||
  width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .cal-event-title {
 | 
			
		||||
  white-space: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
export const environment = {
 | 
			
		||||
  production: true
 | 
			
		||||
  production: true,
 | 
			
		||||
  api: "/api"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
 | 
			
		||||
export const environment = {
 | 
			
		||||
  production: false,
 | 
			
		||||
  api: "api"
 | 
			
		||||
  api: "http://localhost:8080/api"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,7 @@
 | 
			
		||||
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
 | 
			
		||||
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
 | 
			
		||||
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
 | 
			
		||||
  <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
 | 
			
		||||
 </head>
 | 
			
		||||
<body class="mat-typography">
 | 
			
		||||
  <app-root></app-root>
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ info:
 | 
			
		||||
  title: API Title
 | 
			
		||||
  version: '1.0'
 | 
			
		||||
servers:
 | 
			
		||||
  - url: http://localhost:8080/api
 | 
			
		||||
  - url: http://127.0.0.1:8080/api
 | 
			
		||||
paths:
 | 
			
		||||
  /auth/signin:
 | 
			
		||||
    post:
 | 
			
		||||
@ -2421,6 +2421,7 @@ components:
 | 
			
		||||
        - eta
 | 
			
		||||
        - limit
 | 
			
		||||
        - overdue
 | 
			
		||||
        - taskgroupPath
 | 
			
		||||
      additionalProperties: false
 | 
			
		||||
      properties:
 | 
			
		||||
        taskID:
 | 
			
		||||
@ -2448,6 +2449,10 @@ components:
 | 
			
		||||
          type: number
 | 
			
		||||
          description: number in minutes that was already worked on this task
 | 
			
		||||
          example: 10
 | 
			
		||||
        taskgroupPath:
 | 
			
		||||
          type: array
 | 
			
		||||
          items:
 | 
			
		||||
            $ref: '#/components/schemas/TaskgroupEntityInfo'
 | 
			
		||||
    ScheduleStatus:
 | 
			
		||||
      required:
 | 
			
		||||
        - activeMinutes
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user