diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java index 85fd9b93..77e9d768 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java @@ -8,7 +8,9 @@ import de.focusshift.zeiterfassung.usermanagement.UserLocalId; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; +import java.time.Duration; import java.time.LocalDate; +import java.util.AbstractMap; import java.util.Collection; import java.util.List; import java.util.Map; @@ -16,6 +18,8 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import static java.util.stream.Collectors.toMap; + record ReportDay( LocalDate date, Map plannedWorkingHoursByUser, @@ -33,24 +37,33 @@ public PlannedWorkingHours plannedWorkingHours() { public ShouldWorkingHours shouldWorkingHours() { - final double absenceDayLengthValue = detailDayAbsencesByUser.values().stream() - .flatMap(Collection::stream) - .map(ReportDayAbsence::absence) - .map(Absence::dayLength) - .map(DayLength::getValue) - .reduce(0.0, Double::sum); - - if (absenceDayLengthValue >= 1.0) { - return ShouldWorkingHours.ZERO; - } - - final PlannedWorkingHours plannedWorkingHours = plannedWorkingHours(); - - if (absenceDayLengthValue == 0.5) { - return new ShouldWorkingHours(plannedWorkingHours.duration().dividedBy(2)); + final Map absenceLengthByUser = detailDayAbsencesByUser.entrySet().stream() + .map(entry -> + new AbstractMap.SimpleEntry<>( + entry.getKey(), + entry.getValue().stream().map(ReportDayAbsence::absence) + .map(Absence::dayLength) + .map(DayLength::getValue) + .reduce(0.0, Double::sum) + ) + ) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + + Duration plannedWorkingHoursOfAllUsers = plannedWorkingHours().duration(); + for (final Map.Entry absenceLengths : absenceLengthByUser.entrySet()) { + + final Duration plannedWorkingHoursOfUser = plannedWorkingHoursByUser.get(absenceLengths.getKey()).duration(); + final Double absenceLength = absenceLengths.getValue(); + + if (absenceLength == 1.0) { + plannedWorkingHoursOfAllUsers = plannedWorkingHoursOfAllUsers.minus(plannedWorkingHoursOfUser); + } + else if (absenceLength == 0.5) { + plannedWorkingHoursOfAllUsers = plannedWorkingHoursOfAllUsers.minus(plannedWorkingHoursOfUser.dividedBy(2)); + } } - return new ShouldWorkingHours(plannedWorkingHours.duration()); + return new ShouldWorkingHours(plannedWorkingHoursOfAllUsers); } public PlannedWorkingHours plannedWorkingHoursByUser(UserLocalId userLocalId) { diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportDayTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportDayTest.java index 75ec8ae1..6e9f4859 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportDayTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportDayTest.java @@ -1,12 +1,17 @@ package de.focusshift.zeiterfassung.report; +import de.focusshift.zeiterfassung.absence.Absence; +import de.focusshift.zeiterfassung.absence.DayLength; import de.focusshift.zeiterfassung.tenancy.user.EMailAddress; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.user.UserId; import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.usermanagement.User; import de.focusshift.zeiterfassung.usermanagement.UserLocalId; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import java.time.Duration; import java.time.LocalDate; @@ -17,12 +22,19 @@ import java.util.Map; import java.util.Set; +import static de.focusshift.zeiterfassung.absence.AbsenceColor.RED; +import static de.focusshift.zeiterfassung.absence.AbsenceTypeCategory.HOLIDAY; +import static de.focusshift.zeiterfassung.absence.DayLength.FULL; import static org.assertj.core.api.Assertions.assertThat; class ReportDayTest { private static final ZoneId ZONE_ID_BERLIN = ZoneId.of("Europe/Berlin"); + private static ZonedDateTime dateTime(int year, int month, int dayOfMonth, int hour, int minute) { + return ZonedDateTime.of(LocalDateTime.of(year, month, dayOfMonth, hour, minute), ZONE_ID_BERLIN); + } + @Test void ensureToRemoveBreaks() { @@ -40,7 +52,87 @@ void ensureToRemoveBreaks() { assertThat(reportDay.workDuration().duration()).isEqualTo(Duration.ZERO); } - private static ZonedDateTime dateTime(int year, int month, int dayOfMonth, int hour, int minute) { - return ZonedDateTime.of(LocalDateTime.of(year, month, dayOfMonth, hour, minute), ZONE_ID_BERLIN); + @Test + void shouldWorkingHoursReturnsZeroForFullyAbsentUser() { + + final UserId batmanId = new UserId("uuid"); + final UserLocalId batmanLocalId = new UserLocalId(1337L); + final UserIdComposite batmanIdComposite = new UserIdComposite(batmanId, batmanLocalId); + final User batman = new User(batmanIdComposite, "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + + final ZonedDateTime from = dateTime(2021, 1, 4, 1, 0); + final ZonedDateTime to = dateTime(2021, 1, 4, 2, 0); + Absence absence = new Absence(batmanId, from, to, FULL, locale -> "foo", RED, HOLIDAY); + + final ReportDay reportDay = new ReportDay(LocalDate.of(2021, 1, 4), Map.of(batmanIdComposite, PlannedWorkingHours.EIGHT), Map.of(batmanIdComposite, List.of()), Map.of(batmanIdComposite, List.of(new ReportDayAbsence(batman, absence)))); + + assertThat(reportDay.shouldWorkingHours()).isEqualTo(new ShouldWorkingHours(Duration.ZERO)); + } + + @ParameterizedTest + @EnumSource(value = DayLength.class, names = {"MORNING", "NOON"}) + void shouldWorkingHoursReturnsHalfShouldWorkingHoursForHalfDayAbsentUser(DayLength dayLength) { + + final UserId batmanId = new UserId("uuid"); + final UserLocalId batmanLocalId = new UserLocalId(1337L); + final UserIdComposite batmanIdComposite = new UserIdComposite(batmanId, batmanLocalId); + final User batman = new User(batmanIdComposite, "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + + final ZonedDateTime from = dateTime(2021, 1, 4, 1, 0); + final ZonedDateTime to = dateTime(2021, 1, 4, 2, 0); + Absence absence = new Absence(batmanId, from, to, dayLength, locale -> "foo", RED, HOLIDAY); + + final ReportDay reportDay = new ReportDay(LocalDate.of(2021, 1, 4), Map.of(batmanIdComposite, PlannedWorkingHours.EIGHT), Map.of(batmanIdComposite, List.of()), Map.of(batmanIdComposite, List.of(new ReportDayAbsence(batman, absence)))); + + assertThat(reportDay.shouldWorkingHours()).isEqualTo(new ShouldWorkingHours(Duration.ofHours(4))); + } + + @Test + void shouldWorkingHoursReturnsShouldHoursForOneAbsentUserAndOneWorkingUser() { + + final UserId batmanId = new UserId("uuid1"); + final UserLocalId batmanLocalId = new UserLocalId(1337L); + final UserIdComposite batmanIdComposite = new UserIdComposite(batmanId, batmanLocalId); + final User batman = new User(batmanIdComposite, "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + + final ZonedDateTime from = dateTime(2021, 1, 4, 1, 0); + final ZonedDateTime to = dateTime(2021, 1, 4, 2, 0); + Absence absence = new Absence(batmanId, from, to, FULL, locale -> "foo", RED, HOLIDAY); + + final UserId robinId = new UserId("uuid2"); + final UserLocalId robinLocalId = new UserLocalId(1337L); + final UserIdComposite robinIdComposite = new UserIdComposite(robinId, robinLocalId); + + Map> entriesByUser = Map.of(batmanIdComposite, List.of(), robinIdComposite, List.of()); + Map> absencesByUser = Map.of(batmanIdComposite, List.of(new ReportDayAbsence(batman, absence)), robinIdComposite, List.of()); + Map plannedWorkingHoursByUser = Map.of(batmanIdComposite, PlannedWorkingHours.EIGHT, robinIdComposite, new PlannedWorkingHours(Duration.ofHours(4L))); + final ReportDay reportDay = new ReportDay(LocalDate.of(2021, 1, 4), plannedWorkingHoursByUser, entriesByUser, absencesByUser); + + assertThat(reportDay.shouldWorkingHours()).isEqualTo(new ShouldWorkingHours(Duration.ofHours(4L))); + } + + @ParameterizedTest + @EnumSource(value = DayLength.class, names = {"MORNING", "NOON"}) + void shouldWorkingHoursReturnsSummedShouldHoursForOneHalfAbsentUserAndOneWorkingUser(DayLength dayLength) { + + final UserId batmanId = new UserId("uuid1"); + final UserLocalId batmanLocalId = new UserLocalId(1337L); + final UserIdComposite batmanIdComposite = new UserIdComposite(batmanId, batmanLocalId); + final User batman = new User(batmanIdComposite, "Bruce", "Wayne", new EMailAddress("batman@example.org"), Set.of()); + + final ZonedDateTime from = dateTime(2021, 1, 4, 1, 0); + final ZonedDateTime to = dateTime(2021, 1, 4, 2, 0); + Absence absence = new Absence(batmanId, from, to, dayLength, locale -> "foo", RED, HOLIDAY); + + final UserId robinId = new UserId("uuid2"); + final UserLocalId robinLocalId = new UserLocalId(1337L); + final UserIdComposite robinIdComposite = new UserIdComposite(robinId, robinLocalId); + + Map> entriesByUser = Map.of(batmanIdComposite, List.of(), robinIdComposite, List.of()); + Map> absencesByUser = Map.of(batmanIdComposite, List.of(new ReportDayAbsence(batman, absence)), robinIdComposite, List.of()); + Map plannedWorkingHoursByUser = Map.of(batmanIdComposite, PlannedWorkingHours.EIGHT, robinIdComposite, new PlannedWorkingHours(Duration.ofHours(4L))); + final ReportDay reportDay = new ReportDay(LocalDate.of(2021, 1, 4), plannedWorkingHoursByUser, entriesByUser, absencesByUser); + + assertThat(reportDay.shouldWorkingHours()).isEqualTo(new ShouldWorkingHours(Duration.ofHours(8L))); } }