From fab5bd5026e9ef3f6799b99899e3bebda5ae5756 Mon Sep 17 00:00:00 2001 From: jwekesser <118290713+JonathanWekesser@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:49:15 +0100 Subject: [PATCH 1/2] Bug #170 fixed: Active work is now instant shown in Report and added some Unit Tests for Calculations --- .../keeptime/controller/report/Report.java | 108 ++++++++ .../keeptime/view/ReportController.java | 113 +++----- .../report/CalculateReportTest.java | 258 ++++++++++++++++++ 3 files changed, 409 insertions(+), 70 deletions(-) create mode 100644 src/main/java/de/doubleslash/keeptime/controller/report/Report.java create mode 100644 src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java diff --git a/src/main/java/de/doubleslash/keeptime/controller/report/Report.java b/src/main/java/de/doubleslash/keeptime/controller/report/Report.java new file mode 100644 index 00000000..74b7a0d4 --- /dev/null +++ b/src/main/java/de/doubleslash/keeptime/controller/report/Report.java @@ -0,0 +1,108 @@ +package de.doubleslash.keeptime.controller.report; + +import de.doubleslash.keeptime.common.DateFormatter; +import de.doubleslash.keeptime.controller.Controller; +import de.doubleslash.keeptime.model.Model; +import de.doubleslash.keeptime.model.Project; +import de.doubleslash.keeptime.model.Work; + +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; + +public class Report { + private final LocalDate date; + private final Model model; + private final Controller controller; + private List workItems; + private long workItemsSeconds; + private SortedSet workedProjectsSet; + private long presentTime; + private long workTime; + private Map projectWorkSecondsMap; + + public Report(LocalDate date, Model model, Controller controller) { + this.date = date; + this.model = model; + this.controller = controller; + + fetchWorkItems(); + + fetchProjects(); + + calculateSeconds(); + + } + + public LocalDate getDate() { + return date; + } + + public String getDateString() { + return DateFormatter.toDayDateString(this.date); + } + + public List getWorkItems() { + return workItems; + } + + public long getWorkItemsSeconds() { + return workItemsSeconds; + } + + public SortedSet getWorkedProjectsSet() { + return workedProjectsSet; + } + + public String getPresentTimeString() { + return DateFormatter.secondsToHHMMSS(this.presentTime); + } + + public String getWorkTimeString() { + return DateFormatter.secondsToHHMMSS(this.workTime); + } + + public Map getProjectWorkSecondsMap() { + return this.projectWorkSecondsMap; + } + + private void fetchWorkItems() { + this.workItems = model.getWorkRepository().findByStartDateOrderByStartTimeAsc(date); + + if (date.equals(LocalDate.now())) { + Work activeWorkItem = model.activeWorkItem.get(); + if (activeWorkItem != null && !this.workItems.contains(activeWorkItem)) { + this.workItems.add(activeWorkItem); + } + } + } + + private void fetchProjects() { + this.workedProjectsSet = workItems.stream() + .map(Work::getProject) + .collect(Collectors.toCollection( + () -> new TreeSet<>(Comparator.comparing(Project::getIndex)))); + } + + private void calculateSeconds() { + this.workItemsSeconds = this.controller.calcSeconds(this.workItems); + + this.projectWorkSecondsMap = new HashMap<>(); + + for (final Project project : workedProjectsSet) { + final List onlyCurrentProjectWork = workItems.stream() + .filter(w -> w.getProject() == project) + .collect(Collectors.toList()); + + final long projectWorkSeconds = this.controller.calcSeconds(onlyCurrentProjectWork); + + projectWorkSecondsMap.put(project.getId(), projectWorkSeconds); + + presentTime += projectWorkSeconds; + if (project.isWork()) { + workTime += projectWorkSeconds; + } + } + } + +} diff --git a/src/main/java/de/doubleslash/keeptime/view/ReportController.java b/src/main/java/de/doubleslash/keeptime/view/ReportController.java index 71f4e22a..967a8ff9 100644 --- a/src/main/java/de/doubleslash/keeptime/view/ReportController.java +++ b/src/main/java/de/doubleslash/keeptime/view/ReportController.java @@ -16,20 +16,11 @@ package de.doubleslash.keeptime.view; -import java.io.IOException; -import java.time.LocalDate; -import java.util.*; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import de.doubleslash.keeptime.common.DateFormatter; import de.doubleslash.keeptime.common.Resources; import de.doubleslash.keeptime.common.Resources.RESOURCE; import de.doubleslash.keeptime.common.SvgNodeProvider; import de.doubleslash.keeptime.controller.Controller; +import de.doubleslash.keeptime.controller.report.Report; import de.doubleslash.keeptime.exceptions.FXMLLoaderException; import de.doubleslash.keeptime.model.Model; import de.doubleslash.keeptime.model.Project; @@ -57,6 +48,14 @@ import javafx.scene.shape.Circle; import javafx.stage.Stage; import javafx.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; @Component public class ReportController { @@ -70,43 +69,29 @@ public class ReportController { private static final String FX_BACKGROUND_COLOR_WORKED = "-fx-background-color: #00a5e1;"; private static final String EDIT_WORK_DIALOG_TITLE = "Edit work"; - + private static final Logger LOG = LoggerFactory.getLogger(ReportController.class); + private final Model model; + private final Controller controller; + private final TreeItem rootItem = new TreeItem<>(); @FXML private BorderPane topBorderPane; - @FXML private Label currentDayLabel; @FXML private Label currentDayWorkTimeLabel; @FXML private Label currentDayTimeLabel; - @FXML private TreeTableView workTableTreeView; - @FXML private AnchorPane reportRoot; - @FXML private Canvas colorTimeLineCanvas; - @FXML private Button expandCollapseButton; - - private static final Logger LOG = LoggerFactory.getLogger(ReportController.class); - - private final Model model; - - private final Controller controller; - private Stage stage; - private ColorTimeLine colorTimeLine; - private LocalDate currentReportDate; - - private final TreeItem rootItem = new TreeItem<>(); - private boolean expanded = true; public ReportController(final Model model, final Controller controller) { @@ -121,7 +106,7 @@ private void initialize() { colorTimeLine = new ColorTimeLine(colorTimeLineCanvas); - expandCollapseButton.setOnMouseClicked(event ->toggleCollapseExpandReport()); + expandCollapseButton.setOnMouseClicked(event -> toggleCollapseExpandReport()); initTableView(); } @@ -211,74 +196,62 @@ protected void updateItem(TableRow workItem, boolean empty) { rootItem.setExpanded(true); } - private void toggleCollapseExpandReport(){ + private void toggleCollapseExpandReport() { - if(expanded){ + if (expanded) { expandAll(false); expandCollapseButton.setText("Expand"); - }else { + } else { expandAll(true); expandCollapseButton.setText("Collapse"); } expanded = !expanded; } - private void expandAll(boolean expand){ - for (int i=0; i currentWorkItems = model.getWorkRepository() - .findByStartDateOrderByStartTimeAsc(this.currentReportDate); - - colorTimeLine.update(currentWorkItems, controller.calcSeconds(currentWorkItems)); - - final SortedSet workedProjectsSet = currentWorkItems.stream() - .map(Work::getProject) - .collect(Collectors.toCollection(() -> new TreeSet<>( - Comparator.comparing(Project::getIndex)))); - - long currentWorkSeconds = 0; - long currentSeconds = 0; + this.currentDayLabel.setText(report.getDateString()); - for (final Project project : workedProjectsSet) { - final List onlyCurrentProjectWork = currentWorkItems.stream() - .filter(w -> w.getProject() == project) - .collect(Collectors.toList()); + colorTimeLine.update(report.getWorkItems(), report.getWorkItemsSeconds()); - final long projectWorkSeconds = controller.calcSeconds(onlyCurrentProjectWork); - - currentSeconds += projectWorkSeconds; - if (project.isWork()) { - currentWorkSeconds += projectWorkSeconds; - } + for (Project project : report.getWorkedProjectsSet()) { + final List projectWorks = report.getWorkItems() + .stream() + .filter(w -> w.getProject().getId() == project.getId()) + .toList(); final HBox projectButtonBox = new HBox(); - projectButtonBox.getChildren().add(createCopyProjectButton(onlyCurrentProjectWork)); - + projectButtonBox.getChildren().add(createCopyProjectButton(projectWorks)); final TreeItem projectRow = new TreeItem<>( - new ProjectTableRow(project, projectWorkSeconds, projectButtonBox)); + new ProjectTableRow(project, report.getProjectWorkSecondsMap().get(project.getId()), projectButtonBox)); - for (final Work w : onlyCurrentProjectWork) { + for (final Work work : projectWorks) { final HBox workButtonBox = new HBox(5.0); - if(w.getId()==model.activeWorkItem.get().getId()){ + + if (work.getId() == model.activeWorkItem.get().getId() || work.getId() == 0) { Label label = new Label("Active Work"); - label.setTooltip(new Tooltip("The active work item cannot be edited as it is currently active. To edit it you need to switch to another work first.")); + label.setTooltip(new Tooltip( + "The active work item cannot be edited as it is currently active. To edit it you need to switch to another work first.")); label.setStyle("-fx-font-weight: bold"); workButtonBox.getChildren().add(label); - }else { - workButtonBox.getChildren().add(createCopyWorkButton(w)); - workButtonBox.getChildren().add(createEditWorkButton(w)); - workButtonBox.getChildren().add(createDeleteWorkButton(w)); + } else { + workButtonBox.getChildren().add(createCopyWorkButton(work)); + workButtonBox.getChildren().add(createEditWorkButton(work)); + workButtonBox.getChildren().add(createDeleteWorkButton(work)); } - final TreeItem workRow = new TreeItem<>(new WorkTableRow(w, workButtonBox)); + final TreeItem workRow = new TreeItem<>(new WorkTableRow(work, workButtonBox)); projectRow.getChildren().add(workRow); } @@ -287,8 +260,8 @@ private void updateReport(final LocalDate dateToShow) { } - this.currentDayTimeLabel.setText(DateFormatter.secondsToHHMMSS(currentSeconds)); - this.currentDayWorkTimeLabel.setText(DateFormatter.secondsToHHMMSS(currentWorkSeconds)); + this.currentDayTimeLabel.setText(report.getPresentTimeString()); + this.currentDayWorkTimeLabel.setText(report.getWorkTimeString()); loadCalenderWidget(); diff --git a/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java b/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java new file mode 100644 index 00000000..1121874f --- /dev/null +++ b/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java @@ -0,0 +1,258 @@ +package de.doubleslash.keeptime.controller.report; + +import de.doubleslash.keeptime.common.DateFormatter; +import de.doubleslash.keeptime.common.DateProvider; +import de.doubleslash.keeptime.controller.Controller; +import de.doubleslash.keeptime.model.Model; +import de.doubleslash.keeptime.model.Project; +import de.doubleslash.keeptime.model.Work; +import de.doubleslash.keeptime.model.repos.ProjectRepository; +import de.doubleslash.keeptime.model.repos.SettingsRepository; +import de.doubleslash.keeptime.model.repos.WorkRepository; +import javafx.scene.paint.Color; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.testfx.framework.junit5.ApplicationExtension; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(ApplicationExtension.class) +public class CalculateReportTest { + + private static Controller controller; + private Model model; + private DateProvider mockedDateProvider; + private WorkRepository mockedWorkRepository; + + @BeforeEach + void beforeTest() { + mockedWorkRepository = Mockito.mock(WorkRepository.class); + mockedDateProvider = Mockito.mock(DateProvider.class); + model = new Model(Mockito.mock(ProjectRepository.class), mockedWorkRepository, + Mockito.mock(SettingsRepository.class)); + controller = new Controller(model, mockedDateProvider); + } + + @Test + void whenGetReportStructureThenReturnRightDate() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); + String expected = DateFormatter.toDayDateString(date); + + // ACT + Report report = new Report(date, model, controller); + + // ASSERT + assertThat(report.getDateString(), is(expected)); + } + + @Test + void whenNoTimesThenReturnBaseReport() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); + String expectedTime = "00:00:00"; + List emptyWorkList = new ArrayList<>(); + final SortedSet emptyProjectSet = Collections.emptySortedSet(); + Long expectedWorkSeconds = 0L; + + when(model.getWorkRepository().findByStartDateOrderByStartTimeAsc(any())).thenReturn(emptyWorkList); + + // ACT + Report report = new Report(date, model, controller); + + + // ASSERT + assertThat(report.getWorkTimeString(), is(expectedTime)); + assertThat(report.getPresentTimeString(), is(expectedTime)); + assertThat(report.getWorkItems(), is(emptyWorkList)); + assertThat(report.getWorkItemsSeconds(), is(expectedWorkSeconds)); + assertThat(report.getWorkedProjectsSet(), is(emptyProjectSet)); + } + + @Test + void whenOneWorkItemWithNoWorkTime() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); + long presentTime = 60L; + long workTime = 0L; + String expectedPresentTime = DateFormatter.secondsToHHMMSS(presentTime); + String expectedWorkTime = DateFormatter.secondsToHHMMSS(workTime); + + List workList = new ArrayList<>(); + Project project = new Project("Idle", "description", Color.ORANGE, false, 0, true); + LocalDateTime startTime = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 0); + LocalDateTime endTime = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 1, 0); + workList.add(new Work(startTime, endTime, project, "notes")); + + final SortedSet projects = workList.stream() + .map(Work::getProject) + .collect(Collectors.toCollection(() -> new TreeSet<>( + Comparator.comparing(Project::getIndex)))); + + when(model.getWorkRepository().findByStartDateOrderByStartTimeAsc(any())).thenReturn(workList); + + // ACT + Report report = new Report(date, model, controller); + + // ASSERT + assertThat(report.getPresentTimeString(), is(expectedPresentTime)); + assertThat(report.getWorkTimeString(), is(expectedWorkTime)); + assertThat(report.getWorkedProjectsSet(), is(projects)); + assertThat(report.getWorkItems(), is(workList)); + } + + @Test + void whenTwoWorkItemsWithWorkAndPresentTime() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); + long presentTime = 90L; + long workTime = 60L; + String expectedPresentTime = DateFormatter.secondsToHHMMSS(presentTime); + String expectedWorkTime = DateFormatter.secondsToHHMMSS(workTime); + + List workList = new ArrayList<>(); + Project presentProject = new Project("Idle", "description", Color.ORANGE, false, 0, true); + Project workProject = new Project("Project", "description", Color.BLACK, true, 1); + + LocalDateTime startTimePresent = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 0); + LocalDateTime endTimePresent = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 30); + LocalDateTime startTimeWork = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 30); + LocalDateTime endTimeWork = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 1, 30); + + workList.add(new Work(startTimePresent, endTimePresent, presentProject, "notes")); + workList.add(new Work(startTimeWork, endTimeWork, workProject, "notes")); + + final SortedSet projects = workList.stream() + .map(Work::getProject) + .collect(Collectors.toCollection(() -> new TreeSet<>( + Comparator.comparing(Project::getIndex)))); + + when(model.getWorkRepository().findByStartDateOrderByStartTimeAsc(any())).thenReturn(workList); + + // ACT + Report report = new Report(date, model, controller); + + // ASSERT + assertThat(report.getPresentTimeString(), is(expectedPresentTime)); + assertThat(report.getWorkTimeString(), is(expectedWorkTime)); + assertThat(report.getWorkedProjectsSet(), is(projects)); + assertThat(report.getWorkItems(), is(workList)); + } + + @Test + void whenWorkItemsWithWorkThenReturnRightTimes() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); + + List workList = new ArrayList<>(); + Project presentProject = new Project("Idle", "description", Color.ORANGE, false, 0, true); + Project workProject = new Project("Project", "description", Color.BLACK, true, 1); + + LocalDateTime startTimePresent = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 0); + LocalDateTime endTimePresent = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 30); + LocalDateTime startTimeWork = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 0, 30); + LocalDateTime endTimeWork = LocalDateTime.of(date.getYear(), date.getMonth(), date.getDayOfMonth(), 8, 1, 30); + + workList.add(new Work(startTimePresent, endTimePresent, presentProject, "notes")); + workList.add(new Work(startTimeWork, endTimeWork, workProject, "notes")); + + final SortedSet projects = workList.stream() + .map(Work::getProject) + .collect(Collectors.toCollection(() -> new TreeSet<>( + Comparator.comparing(Project::getIndex)))); + + Map map = new HashMap<>(); + for (final Project project : projects) { + final List onlyCurrentProjectWork = workList.stream() + .filter(w -> w.getProject() == project) + .collect(Collectors.toList()); + + final long projectWorkSeconds = controller.calcSeconds(onlyCurrentProjectWork); + map.put(project.getId(), projectWorkSeconds); + } + + when(model.getWorkRepository().findByStartDateOrderByStartTimeAsc(any())).thenReturn(workList); + + // ACT + Report report = new Report(date, model, controller); + + // ASSERT + assertThat(report.getProjectWorkSecondsMap(), is(map)); + } + + @Test + void whenDayIsTodayThenShowActiveWork() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); + + LocalDateTime now = mockedDateProvider.dateTimeNow(); + LocalDateTime startTime = mockedDateProvider.dateTimeNow().minusSeconds(120); + LocalDateTime endTime = mockedDateProvider.dateTimeNow().minusSeconds(10); + + List workList = new ArrayList<>(); + Project presentProject = new Project("Idle", "description", Color.ORANGE, false, 0, true); + workList.add(new Work(startTime, endTime, presentProject, "notes")); + + Project newProject = new Project("project", "description", Color.WHITE, true, 1); + Work unsavedWork = new Work(endTime, endTime, newProject, ""); + model.activeWorkItem.set(unsavedWork); + + // ACT + Report report = new Report(date, model, controller); + + // ASSERT + assertThat(report.getWorkItems(), contains(unsavedWork)); + assertThat(report.getWorkedProjectsSet(), contains(newProject)); + } + + @Test + void whenDayIsNotTodayThenDoNotShowActiveWork() { + // ARRANGE + when(mockedDateProvider.dateTimeNow()).thenReturn(LocalDateTime.now()); + + LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate().minusDays(1); + + LocalDateTime now = mockedDateProvider.dateTimeNow(); + LocalDateTime startTime = mockedDateProvider.dateTimeNow().minusDays(1).minusSeconds(120); + LocalDateTime endTime = mockedDateProvider.dateTimeNow().minusDays(1).minusSeconds(10); + + List workList = new ArrayList<>(); + Project presentProject = new Project("Idle", "description", Color.ORANGE, false, 0, true); + workList.add(new Work(startTime, endTime, presentProject, "notes")); + + Project newProject = new Project("project", "description", Color.WHITE, true, 1); + Work unsavedWork = new Work(now.minusSeconds(10), now.minusSeconds(10), newProject, ""); + model.activeWorkItem.set(unsavedWork); + + when(model.getWorkRepository().findByStartDateOrderByStartTimeAsc(any())).thenReturn(workList); + + // ACT + Report report = new Report(date, model, controller); + + // ASSERT + assertThat(report.getWorkItems(), not(contains(unsavedWork))); + assertThat(report.getWorkedProjectsSet(), not(contains(newProject))); + } +} From daeebeaf80fbfe5408f7cc505b770691eb844c4e Mon Sep 17 00:00:00 2001 From: jwekesser <118290713+JonathanWekesser@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:52:29 +0100 Subject: [PATCH 2/2] #170 deleted my warnings --- .../keeptime/controller/report/CalculateReportTest.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java b/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java index 1121874f..f74e5b0d 100644 --- a/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java +++ b/src/test/java/de/doubleslash/keeptime/controller/report/CalculateReportTest.java @@ -32,11 +32,10 @@ public class CalculateReportTest { private static Controller controller; private Model model; private DateProvider mockedDateProvider; - private WorkRepository mockedWorkRepository; @BeforeEach void beforeTest() { - mockedWorkRepository = Mockito.mock(WorkRepository.class); + WorkRepository mockedWorkRepository = Mockito.mock(WorkRepository.class); mockedDateProvider = Mockito.mock(DateProvider.class); model = new Model(Mockito.mock(ProjectRepository.class), mockedWorkRepository, Mockito.mock(SettingsRepository.class)); @@ -207,14 +206,8 @@ void whenDayIsTodayThenShowActiveWork() { LocalDate date = mockedDateProvider.dateTimeNow().toLocalDate(); - LocalDateTime now = mockedDateProvider.dateTimeNow(); - LocalDateTime startTime = mockedDateProvider.dateTimeNow().minusSeconds(120); LocalDateTime endTime = mockedDateProvider.dateTimeNow().minusSeconds(10); - List workList = new ArrayList<>(); - Project presentProject = new Project("Idle", "description", Color.ORANGE, false, 0, true); - workList.add(new Work(startTime, endTime, presentProject, "notes")); - Project newProject = new Project("project", "description", Color.WHITE, true, 1); Work unsavedWork = new Work(endTime, endTime, newProject, ""); model.activeWorkItem.set(unsavedWork);