From b7598025d03b63b57b005c74bb1a7817ae8fb5b0 Mon Sep 17 00:00:00 2001 From: Benjamin Seber Date: Fri, 11 Oct 2024 11:54:49 +0200 Subject: [PATCH] add numbers/graph-bar to month report --- .../zeiterfassung/report/GraphMonthDto.java | 11 +- .../zeiterfassung/report/ReportMonth.java | 41 +++++- .../report/ReportMonthController.java | 23 +++- .../templates/reports/user-report-month.html | 65 ++++++++- .../templates/reports/user-report-week.html | 2 - .../report/ReportMonthControllerTest.java | 124 +++++++++++++++++- 6 files changed, 251 insertions(+), 15 deletions(-) diff --git a/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java b/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java index d7031e4df..fa6caf554 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java @@ -2,7 +2,16 @@ import java.util.List; -record GraphMonthDto(String yearMonth, List weekReports, Double maxHoursWorked) { +record GraphMonthDto( + String yearMonth, + List weekReports, + Double maxHoursWorked, + String workedWorkingHours, + String shouldWorkingHours, + String hoursDelta, + boolean hoursDeltaNegative, + double hoursWorkedRatio +) { public Double graphLegendMaxHour() { diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java index 56093f8f6..248c0f29c 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java @@ -1,17 +1,32 @@ package de.focusshift.zeiterfassung.report; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; +import java.math.BigDecimal; import java.time.Duration; import java.time.YearMonth; import java.util.Collection; import java.util.List; +import static java.math.RoundingMode.CEILING; import static java.util.function.Predicate.not; record ReportMonth(YearMonth yearMonth, List weeks) { + public PlannedWorkingHours plannedWorkingHours() { + return weeks.stream() + .map(ReportWeek::plannedWorkingHours) + .reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus); + } + + public ShouldWorkingHours shouldWorkingHours() { + return weeks.stream() + .map(ReportWeek::shouldWorkingHours) + .reduce(ShouldWorkingHours.ZERO, ShouldWorkingHours::plus); + } + public WorkDuration averageDayWorkDuration() { final double averageMinutes = weeks.stream() @@ -29,9 +44,27 @@ public WorkDuration averageDayWorkDuration() { return new WorkDuration(duration); } - public PlannedWorkingHours plannedWorkingHours() { - return weeks.stream() - .map(ReportWeek::plannedWorkingHours) - .reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus); + public WorkDuration workDuration() { + return weeks + .stream() + .map(ReportWeek::workDuration) + .reduce(WorkDuration.ZERO, WorkDuration::plus); + } + + public BigDecimal workedHoursRatio() { + + final double planned = shouldWorkingHours().durationInMinutes().toMinutes(); + final double worked = workDuration().durationInMinutes().toMinutes(); + + if (worked == 0) { + return BigDecimal.ZERO; + } + + if (planned == 0) { + return BigDecimal.ONE; + } + + final BigDecimal ratio = BigDecimal.valueOf(worked).divide(BigDecimal.valueOf(planned), 2, CEILING); + return ratio.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : ratio; } } diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java index 1fb3ced8d..d09568746 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java @@ -2,6 +2,8 @@ import de.focus_shift.launchpad.api.HasLaunchpad; import de.focusshift.zeiterfassung.timeclock.HasTimeClock; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; +import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.user.DateFormatter; import de.focusshift.zeiterfassung.usermanagement.UserLocalId; import jakarta.servlet.http.HttpServletRequest; @@ -18,8 +20,11 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import java.math.BigDecimal; +import java.math.MathContext; import java.time.Clock; import java.time.DateTimeException; +import java.time.Duration; import java.time.YearMonth; import java.util.List; import java.util.Locale; @@ -142,7 +147,17 @@ private GraphMonthDto toGraphMonthDto(ReportMonth reportMonth) { .mapToDouble(value -> value) .max().orElse(0.0); - return new GraphMonthDto(yearMonth, graphWeekDtos, maxHoursWorked); + final WorkDuration workDuration = reportMonth.workDuration(); + final ShouldWorkingHours shouldWorkingHours = reportMonth.shouldWorkingHours(); + final String shouldWorkingHoursString = durationToTimeString(shouldWorkingHours.duration()); + final String workedWorkingHoursString = durationToTimeString(workDuration.duration()); + + final Duration deltaDuration = workDuration.duration().minus(shouldWorkingHours.duration()); + final String deltaHours = durationToTimeString(deltaDuration); + + final double weekRatio = reportMonth.workedHoursRatio().multiply(BigDecimal.valueOf(100), new MathContext(2)).doubleValue(); + + return new GraphMonthDto(yearMonth, graphWeekDtos, maxHoursWorked, workedWorkingHoursString, shouldWorkingHoursString, deltaHours, deltaDuration.isNegative(), weekRatio); } private DetailMonthDto toDetailMonthDto(ReportMonth reportMonth, Locale locale) { @@ -165,4 +180,10 @@ private static Optional yearMonth(int year, int month) { return Optional.empty(); } } + + private static String durationToTimeString(Duration duration) { + // use positive values to format duration string + // negative value is handled in template + return String.format("%02d:%02d", Math.abs(duration.toHours()), Math.abs(duration.toMinutesPart())); + } } diff --git a/src/main/resources/templates/reports/user-report-month.html b/src/main/resources/templates/reports/user-report-month.html index 25ffd686d..6d4a75e90 100644 --- a/src/main/resources/templates/reports/user-report-month.html +++ b/src/main/resources/templates/reports/user-report-month.html @@ -72,9 +72,68 @@ >Dieser Monat - - Dezember 2021 - +
+
+
+ + Oktober 2024 + +

+ + - + + + + 01:30 + + + + + +

+

+ Geleistet: + + 41:58 + + (Soll: + 40:00 ) +

+
+
+
+
+
+
+
+
- -
-
diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java index 0a1dae501..e84a8db0e 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java @@ -87,7 +87,33 @@ void ensureReportMonth() throws Exception { final ReportMonth reportMonth = new ReportMonth( YearMonth.of(2023, 2), List.of( - fourtyHourWeek(user, LocalDate.of(2023, 1, 30)) + new ReportWeek( + LocalDate.of(2023, 1, 30), + List.of( + zeroHoursDay(LocalDate.of(2023, 1, 30), user), + zeroHoursDay(LocalDate.of(2023, 1, 31), user), + eightHoursDay(LocalDate.of(2023, 2, 1), user), + eightHoursDay(LocalDate.of(2023, 2, 2), user), + eightHoursDay(LocalDate.of(2023, 2, 3), user), + zeroHoursDay(LocalDate.of(2023, 2, 4), user), + zeroHoursDay(LocalDate.of(2023, 2, 5), user) + ) + ), + fourtyHourWeek(user, LocalDate.of(2023, 2, 6)), + fourtyHourWeek(user, LocalDate.of(2023, 2, 13)), + fourtyHourWeek(user, LocalDate.of(2023, 2, 20)), + new ReportWeek( + LocalDate.of(2023, 2, 27), + List.of( + eightHoursDay(LocalDate.of(2023, 2, 27), user), + eightHoursDay(LocalDate.of(2023, 2, 28), user), + zeroHoursDay(LocalDate.of(2023, 3, 1), user), + zeroHoursDay(LocalDate.of(2023, 3, 2), user), + zeroHoursDay(LocalDate.of(2023, 3, 3), user), + zeroHoursDay(LocalDate.of(2023, 3, 4), user), + zeroHoursDay(LocalDate.of(2023, 3, 5), user) + ) + ) ) ); @@ -101,8 +127,8 @@ void ensureReportMonth() throws Exception { 5, "date-range", List.of( - new GraphDayDto(true, "M", "Montag", "30.01.2023", 8d, 8d), - new GraphDayDto(true, "D", "Dienstag", "31.01.2023", 8d, 8d), + new GraphDayDto(true, "M", "Montag", "30.01.2023", 0d, 0d), + new GraphDayDto(true, "D", "Dienstag", "31.01.2023", 0d, 0d), new GraphDayDto(false, "M", "Mittwoch", "01.02.2023", 8d, 8d), new GraphDayDto(false, "D", "Donnerstag", "02.02.2023", 8d, 8d), new GraphDayDto(false, "F", "Freitag", "03.02.2023", 8d, 8d), @@ -110,14 +136,95 @@ void ensureReportMonth() throws Exception { new GraphDayDto(false, "S", "Sonntag", "05.02.2023", 0d, 0d) ), 8d, + "24:00", + "24:00", + "00:00", + false, + 100d + ), + new GraphWeekDto( + 6, + "date-range", + List.of( + new GraphDayDto(false, "M", "Montag", "06.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Dienstag", "07.02.2023", 8d, 8d), + new GraphDayDto(false, "M", "Mittwoch", "08.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Donnerstag", "09.02.2023", 8d, 8d), + new GraphDayDto(false, "F", "Freitag", "10.02.2023", 8d, 8d), + new GraphDayDto(false, "S", "Samstag", "11.02.2023", 0d, 0d), + new GraphDayDto(false, "S", "Sonntag", "12.02.2023", 0d, 0d) + ), + 8d, "40:00", "40:00", "00:00", false, 100d + ), + new GraphWeekDto( + 7, + "date-range", + List.of( + new GraphDayDto(false, "M", "Montag", "13.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Dienstag", "14.02.2023", 8d, 8d), + new GraphDayDto(false, "M", "Mittwoch", "15.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Donnerstag", "16.02.2023", 8d, 8d), + new GraphDayDto(false, "F", "Freitag", "17.02.2023", 8d, 8d), + new GraphDayDto(false, "S", "Samstag", "18.02.2023", 0d, 0d), + new GraphDayDto(false, "S", "Sonntag", "19.02.2023", 0d, 0d) + ), + 8d, + "40:00", + "40:00", + "00:00", + false, + 100d + ), + new GraphWeekDto( + 8, + "date-range", + List.of( + new GraphDayDto(false, "M", "Montag", "20.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Dienstag", "21.02.2023", 8d, 8d), + new GraphDayDto(false, "M", "Mittwoch", "22.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Donnerstag", "23.02.2023", 8d, 8d), + new GraphDayDto(false, "F", "Freitag", "24.02.2023", 8d, 8d), + new GraphDayDto(false, "S", "Samstag", "25.02.2023", 0d, 0d), + new GraphDayDto(false, "S", "Sonntag", "26.02.2023", 0d, 0d) + ), + 8d, + "40:00", + "40:00", + "00:00", + false, + 100d + ), + new GraphWeekDto( + 9, + "date-range", + List.of( + new GraphDayDto(false, "M", "Montag", "27.02.2023", 8d, 8d), + new GraphDayDto(false, "D", "Dienstag", "28.02.2023", 8d, 8d), + new GraphDayDto(true, "M", "Mittwoch", "01.03.2023", 0d, 0d), + new GraphDayDto(true, "D", "Donnerstag", "02.03.2023", 0d, 0d), + new GraphDayDto(true, "F", "Freitag", "03.03.2023", 0d, 0d), + new GraphDayDto(true, "S", "Samstag", "04.03.2023", 0d, 0d), + new GraphDayDto(true, "S", "Sonntag", "05.03.2023", 0d, 0d) + ), + 8d, + "16:00", + "16:00", + "00:00", + false, + 100d ) ), - 8d + 8d, + "160:00", + "160:00", + "00:00", + false, + 100d ); perform( @@ -380,6 +487,15 @@ private ReportDay eightHoursDay(LocalDate date, User user) { ); } + private ReportDay zeroHoursDay(LocalDate date, User user) { + return new ReportDay( + date, + Map.of(user.userIdComposite(), PlannedWorkingHours.ZERO), + Map.of(user.userIdComposite(), List.of()), + Map.of(user.userIdComposite(), List.of()) + ); + } + private ReportDayEntry reportDayEntry(User user, LocalDate date) { return new ReportDayEntry(user, "", date.atStartOfDay().plusHours(8).atZone(UTC), date.atStartOfDay().plusHours(16).atZone(UTC), false); }