diff --git a/src/main/css/reports.css b/src/main/css/reports.css index f00cb7fe6..e1d792c85 100644 --- a/src/main/css/reports.css +++ b/src/main/css/reports.css @@ -83,3 +83,27 @@ height: 3rem; @apply rounded-lg; } + +.report-summary-grid { + dt:not(:first-of-type) { + margin-top: 1rem; + } + + dd { + font-weight: 600; + font-variant-numeric: tabular-nums; + white-space: nowrap; + } +} + +@screen sm { + .report-summary-grid { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 1rem; + + dt:not(:first-of-type) { + margin-top: 0; + } + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/report/DetailDayDto.java b/src/main/java/de/focusshift/zeiterfassung/report/DetailDayDto.java index 07b23d807..9b8535922 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/DetailDayDto.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/DetailDayDto.java @@ -1,6 +1,5 @@ package de.focusshift.zeiterfassung.report; -import java.time.Duration; import java.util.List; record DetailDayDto( @@ -8,7 +7,10 @@ record DetailDayDto( String dayOfWeek, String dayOfWeekFull, String date, - Duration hoursWorked, + String workedWorkingHours, + String shouldWorkingHours, + String hoursDelta, + boolean hoursDeltaNegative, List dayEntries, List absences ) { diff --git a/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java b/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java index fd4871efe..fa6caf554 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/GraphMonthDto.java @@ -2,8 +2,16 @@ import java.util.List; -record GraphMonthDto(String yearMonth, List weekReports, Double maxHoursWorked, - Double averageHoursWorked) { +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/GraphWeekDto.java b/src/main/java/de/focusshift/zeiterfassung/report/GraphWeekDto.java index 3d33650a0..39ac9e458 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/GraphWeekDto.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/GraphWeekDto.java @@ -3,10 +3,15 @@ import java.util.List; record GraphWeekDto( - String yearMonthWeek, + int calendarWeek, + String dateRangeString, List dayReports, Double maxHoursWorked, - Double averageHoursWorked + String workedWorkingHours, + String shouldWorkingHours, + String hoursDelta, + boolean hoursDeltaNegative, + double hoursWorkedRatio ) { public Double graphLegendMaxHour() { diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java index 7bc751b3c..7341bd5d9 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java @@ -1,7 +1,10 @@ package de.focusshift.zeiterfassung.report; import de.focusshift.zeiterfassung.absence.Absence; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; +import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.user.DateFormatter; +import de.focusshift.zeiterfassung.user.DateRangeFormatter; import de.focusshift.zeiterfassung.user.UserId; import de.focusshift.zeiterfassung.usermanagement.User; import de.focusshift.zeiterfassung.usermanagement.UserLocalId; @@ -9,6 +12,8 @@ import org.springframework.stereotype.Component; import org.springframework.ui.Model; +import java.math.BigDecimal; +import java.math.MathContext; import java.time.Duration; import java.time.LocalDate; import java.time.LocalTime; @@ -28,10 +33,12 @@ class ReportControllerHelper { private final ReportPermissionService reportPermissionService; private final DateFormatter dateFormatter; + private final DateRangeFormatter dateRangeFormatter; - ReportControllerHelper(ReportPermissionService reportPermissionService, DateFormatter dateFormatter) { + ReportControllerHelper(ReportPermissionService reportPermissionService, DateFormatter dateFormatter, DateRangeFormatter dateRangeFormatter) { this.reportPermissionService = reportPermissionService; this.dateFormatter = dateFormatter; + this.dateRangeFormatter = dateRangeFormatter; } UserId principalToUserId(OidcUser principal) { @@ -65,16 +72,31 @@ GraphWeekDto toGraphWeekDto(ReportWeek reportWeek, Month monthPivot) { .map(reportDay -> toUserReportDayReportDto(reportDay, !reportDay.date().getMonth().equals(monthPivot))) .toList(); - final String yearMonthWeek = dateFormatter.formatYearMonthWeek(reportWeek.firstDateOfWeek()); + final int calendarWeek = reportWeek.firstDateOfWeek().get(ChronoField.ALIGNED_WEEK_OF_YEAR); + final String dateRangeString = dateRangeFormatter.toDateRangeString(reportWeek.firstDateOfWeek(), reportWeek.lastDateOfWeek()); final double maxHoursWorked = dayReports.stream() .map(GraphDayDto::hoursWorked) .mapToDouble(value -> value) .max().orElse(0.0); - final double hoursWorkedAverageADay = reportWeek.averageDayWorkDuration().hoursDoubleValue(); + final WorkDuration workDuration = reportWeek.workDuration(); + final ShouldWorkingHours shouldWorkingHours = reportWeek.shouldWorkingHours(); + final String shouldWorkingHoursString = durationToTimeString(shouldWorkingHours.duration()); + final String workedWorkingHoursString = durationToTimeString(workDuration.duration()); - return new GraphWeekDto(yearMonthWeek, dayReports, maxHoursWorked, hoursWorkedAverageADay); + final Duration deltaDuration = workDuration.duration().minus(shouldWorkingHours.duration()); + final String deltaHours = durationToTimeString(deltaDuration); + + final double weekRatio = reportWeek.workedHoursRatio().multiply(BigDecimal.valueOf(100), new MathContext(2)).doubleValue(); + + return new GraphWeekDto(calendarWeek, dateRangeString, dayReports, maxHoursWorked, workedWorkingHoursString, shouldWorkingHoursString, deltaHours, deltaDuration.isNegative(), weekRatio); + } + + 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())); } private GraphDayDto toUserReportDayReportDto(ReportDay reportDay, boolean differentMonth) { @@ -136,7 +158,6 @@ private DetailDayDto toDetailDayReportDto(ReportDay reportDay, boolean different final String dayOfWeekNarrow = dateFormatter.formatDayOfWeekNarrow(reportDay.date().getDayOfWeek()); final String dayOfWeekFull = dateFormatter.formatDayOfWeekFull(reportDay.date().getDayOfWeek()); final String dateString = dateFormatter.formatDate(reportDay.date()); - final Duration hoursWorked = reportDay.workDuration().duration(); final List dayEntryDtos = reportDay.reportDayEntries().stream().map(this::toDetailDayEntryDto).toList(); final List detailDayAbsenceDto = reportDay.detailDayAbsencesByUser().values().stream() @@ -144,7 +165,15 @@ private DetailDayDto toDetailDayReportDto(ReportDay reportDay, boolean different .map(reportDayAbsence -> toDetailDayAbsenceDto(reportDayAbsence, locale)) .toList(); - return new DetailDayDto(differentMonth, dayOfWeekNarrow, dayOfWeekFull, dateString, hoursWorked, dayEntryDtos, detailDayAbsenceDto); + final WorkDuration workDuration = reportDay.workDuration(); + final ShouldWorkingHours shouldWorkingHours = reportDay.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); + + return new DetailDayDto(differentMonth, dayOfWeekNarrow, dayOfWeekFull, dateString, workedWorkingHoursString, shouldWorkingHoursString, deltaHours, deltaDuration.isNegative(), dayEntryDtos, detailDayAbsenceDto); } private DetailDayEntryDto toDetailDayEntryDto(ReportDayEntry reportDayEntry) { diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java index d54e2ca36..85fd9b932 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java @@ -1,5 +1,8 @@ package de.focusshift.zeiterfassung.report; +import de.focusshift.zeiterfassung.absence.Absence; +import de.focusshift.zeiterfassung.absence.DayLength; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.usermanagement.UserLocalId; @@ -18,7 +21,6 @@ record ReportDay( Map plannedWorkingHoursByUser, Map> reportDayEntriesByUser, Map> detailDayAbsencesByUser - ) { public List reportDayEntries() { @@ -29,6 +31,28 @@ public PlannedWorkingHours plannedWorkingHours() { return plannedWorkingHoursByUser.values().stream().reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus); } + 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)); + } + + return new ShouldWorkingHours(plannedWorkingHours.duration()); + } + public PlannedWorkingHours plannedWorkingHoursByUser(UserLocalId userLocalId) { return findValueByFirstKeyMatch(plannedWorkingHoursByUser, userIdComposite -> userLocalId.equals(userIdComposite.localId())) .orElse(PlannedWorkingHours.ZERO); diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java index 56093f8f6..a58228128 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java @@ -1,5 +1,7 @@ package de.focusshift.zeiterfassung.report; +import de.focusshift.zeiterfassung.timeentry.HasWorkedHoursRatio; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; @@ -10,7 +12,19 @@ import static java.util.function.Predicate.not; -record ReportMonth(YearMonth yearMonth, List weeks) { +record ReportMonth(YearMonth yearMonth, List weeks) implements HasWorkedHoursRatio { + + 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() { @@ -29,9 +43,10 @@ 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); } } diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java index b4a4b0fb2..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,9 +147,17 @@ private GraphMonthDto toGraphMonthDto(ReportMonth reportMonth) { .mapToDouble(value -> value) .max().orElse(0.0); - final double hoursWorkedAverageADay = reportMonth.averageDayWorkDuration().hoursDoubleValue(); + final WorkDuration workDuration = reportMonth.workDuration(); + final ShouldWorkingHours shouldWorkingHours = reportMonth.shouldWorkingHours(); + final String shouldWorkingHoursString = durationToTimeString(shouldWorkingHours.duration()); + final String workedWorkingHoursString = durationToTimeString(workDuration.duration()); - return new GraphMonthDto(yearMonth, graphWeekDtos, maxHoursWorked, hoursWorkedAverageADay); + 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) { @@ -167,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/java/de/focusshift/zeiterfassung/report/ReportWeek.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java index 9a467260f..d8af5c6e3 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java @@ -1,5 +1,7 @@ package de.focusshift.zeiterfassung.report; +import de.focusshift.zeiterfassung.timeentry.HasWorkedHoursRatio; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; @@ -9,7 +11,7 @@ import static java.util.function.Predicate.not; -record ReportWeek(LocalDate firstDateOfWeek, List reportDays) { +record ReportWeek(LocalDate firstDateOfWeek, List reportDays) implements HasWorkedHoursRatio { public PlannedWorkingHours plannedWorkingHours() { return reportDays.stream() @@ -17,6 +19,12 @@ public PlannedWorkingHours plannedWorkingHours() { .reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus); } + public ShouldWorkingHours shouldWorkingHours() { + return reportDays.stream() + .map(ReportDay::shouldWorkingHours) + .reduce(ShouldWorkingHours.ZERO, ShouldWorkingHours::plus); + } + public WorkDuration averageDayWorkDuration() { final double averageMinutes = reportDays().stream() diff --git a/src/main/java/de/focusshift/zeiterfassung/timeentry/HasWorkedHoursRatio.java b/src/main/java/de/focusshift/zeiterfassung/timeentry/HasWorkedHoursRatio.java new file mode 100644 index 000000000..01e2d803b --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/timeentry/HasWorkedHoursRatio.java @@ -0,0 +1,28 @@ +package de.focusshift.zeiterfassung.timeentry; + +import java.math.BigDecimal; + +import static java.math.RoundingMode.CEILING; + +public interface HasWorkedHoursRatio { + + WorkDuration workDuration(); + + ShouldWorkingHours shouldWorkingHours(); + + default BigDecimal workedHoursRatio() { + + final double worked = workDuration().durationInMinutes().toMinutes(); + if (worked == 0) { + return BigDecimal.ZERO; + } + + final double should = shouldWorkingHours().durationInMinutes().toMinutes(); + if (should == 0) { + return BigDecimal.ONE; + } + + final BigDecimal ratio = BigDecimal.valueOf(worked).divide(BigDecimal.valueOf(should), 2, CEILING); + return ratio.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : ratio; + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/timeentry/ShouldWorkingHours.java b/src/main/java/de/focusshift/zeiterfassung/timeentry/ShouldWorkingHours.java index ec7daa0a7..d1745d6df 100644 --- a/src/main/java/de/focusshift/zeiterfassung/timeentry/ShouldWorkingHours.java +++ b/src/main/java/de/focusshift/zeiterfassung/timeentry/ShouldWorkingHours.java @@ -1,11 +1,16 @@ package de.focusshift.zeiterfassung.timeentry; +import de.focusshift.zeiterfassung.absence.Absence; +import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; import de.focusshift.zeiterfassung.workingtime.ZeitDuration; import java.time.Duration; /** - * Hours that should be worked. (e.g. PlannedWorkingHours 40h - Absence 8h = ShouldWorkingHours 32h) + * Hours that should be worked. + * + *

+ * (e.g. {@linkplain PlannedWorkingHours} 40h - {@linkplain Absence} 8h = ShouldWorkingHours 32h) * * @param duration the exact duration. not rounded up to minutes. */ diff --git a/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryDay.java b/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryDay.java index 9fd1e8367..1d57c3006 100644 --- a/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryDay.java +++ b/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryDay.java @@ -3,13 +3,10 @@ import de.focusshift.zeiterfassung.absence.Absence; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; -import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDate; import java.util.List; -import static java.math.RoundingMode.CEILING; - /** * * @param date of the time entry day @@ -24,7 +21,7 @@ record TimeEntryDay( ShouldWorkingHours shouldWorkingHours, List timeEntries, List absences -) { +) implements HasWorkedHoursRatio { /** * @@ -40,27 +37,4 @@ public WorkDuration workDuration() { .map(TimeEntry::workDuration) .reduce(WorkDuration.ZERO, WorkDuration::plus); } - - /** - * Ratio of worked hours to planned hours. Does not include absences like public holidays. - * - * @return value between 0 and 1 - */ - public BigDecimal workedHoursRatio() { - - final double should = shouldWorkingHours.hoursDoubleValue(); - final double worked = workDuration().hoursDoubleValue(); - - if (worked == 0) { - return BigDecimal.ZERO; - } - - if (should == 0) { - return BigDecimal.ONE; - } - - final BigDecimal ratio = BigDecimal.valueOf(worked).divide(BigDecimal.valueOf(should), 2, CEILING); - - return ratio.compareTo(BigDecimal.ONE) > 0 ? BigDecimal.ONE : ratio; - } } diff --git a/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryServiceImpl.java b/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryServiceImpl.java index b716dccc3..2ebef9103 100644 --- a/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryServiceImpl.java +++ b/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryServiceImpl.java @@ -247,7 +247,6 @@ private static ShouldWorkingHours dayShouldHoursWorked(PlannedWorkingHours plann } return new ShouldWorkingHours(plannedWorkingHours.duration()); - } private void updateEntityTimeSpan(TimeEntryEntity entity, ZonedDateTime start, ZonedDateTime end, Duration duration) diff --git a/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryWeek.java b/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryWeek.java index 666737eaa..7ea3acea9 100644 --- a/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryWeek.java +++ b/src/main/java/de/focusshift/zeiterfassung/timeentry/TimeEntryWeek.java @@ -3,19 +3,21 @@ import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; import org.threeten.extra.YearWeek; -import java.math.BigDecimal; import java.time.Duration; import java.time.LocalDate; import java.time.Year; import java.util.Collection; import java.util.List; -import static java.math.RoundingMode.CEILING; import static java.time.Month.DECEMBER; import static java.time.temporal.WeekFields.ISO; import static java.util.function.Predicate.not; -record TimeEntryWeek(LocalDate firstDateOfWeek, PlannedWorkingHours plannedWorkingHours, List days) { +record TimeEntryWeek( + LocalDate firstDateOfWeek, + PlannedWorkingHours plannedWorkingHours, + List days +) implements HasWorkedHoursRatio { public ShouldWorkingHours shouldWorkingHours() { return days.stream().map(TimeEntryDay::shouldWorkingHours).reduce(ShouldWorkingHours.ZERO, ShouldWorkingHours::plus); @@ -44,28 +46,6 @@ public WorkDuration workDuration() { .reduce(WorkDuration.ZERO, WorkDuration::plus); } - /** - * Ratio of worked hours to planned hours. Does not include absences like public holidays. - * - * @return value between 0 and 1 - */ - 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; - } - public int year() { return firstDateOfWeek.getYear(); } diff --git a/src/main/java/de/focusshift/zeiterfassung/timeentry/WorkDuration.java b/src/main/java/de/focusshift/zeiterfassung/timeentry/WorkDuration.java index f88bdfe71..4fd27be8b 100644 --- a/src/main/java/de/focusshift/zeiterfassung/timeentry/WorkDuration.java +++ b/src/main/java/de/focusshift/zeiterfassung/timeentry/WorkDuration.java @@ -12,6 +12,7 @@ public record WorkDuration(Duration duration) implements ZeitDuration { public static final WorkDuration ZERO = new WorkDuration(Duration.ZERO); + public static final WorkDuration EIGHT = new WorkDuration(Duration.ofHours(8)); /** * Returns a copy of this workDuration with the specified workDuration added. diff --git a/src/main/java/de/focusshift/zeiterfassung/user/DateFormatter.java b/src/main/java/de/focusshift/zeiterfassung/user/DateFormatter.java index e41cad5c9..5ffff3543 100644 --- a/src/main/java/de/focusshift/zeiterfassung/user/DateFormatter.java +++ b/src/main/java/de/focusshift/zeiterfassung/user/DateFormatter.java @@ -19,6 +19,4 @@ public interface DateFormatter { String formatDate(LocalDate date); String formatDate(LocalDate date, MonthFormat monthFormat, YearFormat yearFormat); - - String formatYearMonthWeek(LocalDate date); } diff --git a/src/main/java/de/focusshift/zeiterfassung/user/DateFormatterImpl.java b/src/main/java/de/focusshift/zeiterfassung/user/DateFormatterImpl.java index 114358ee6..a29bbbfa6 100644 --- a/src/main/java/de/focusshift/zeiterfassung/user/DateFormatterImpl.java +++ b/src/main/java/de/focusshift/zeiterfassung/user/DateFormatterImpl.java @@ -8,7 +8,6 @@ import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.format.TextStyle; -import java.time.temporal.ChronoField; import java.util.Locale; @Service @@ -53,11 +52,6 @@ public String formatDate(LocalDate date, MonthFormat monthFormat, YearFormat yea return DateTimeFormatter.ofPattern(pattern, locale).format(date); } - public String formatYearMonthWeek(LocalDate date) { - final String yearMonth = formatYearMonth(YearMonth.from(date)); - return yearMonth + " KW " + date.get(ChronoField.ALIGNED_WEEK_OF_YEAR); - } - private Locale locale() { return LocaleContextHolder.getLocale(); } diff --git a/src/main/java/de/focusshift/zeiterfassung/user/DateRangeFormatter.java b/src/main/java/de/focusshift/zeiterfassung/user/DateRangeFormatter.java new file mode 100644 index 000000000..8275ffcc2 --- /dev/null +++ b/src/main/java/de/focusshift/zeiterfassung/user/DateRangeFormatter.java @@ -0,0 +1,38 @@ +package de.focusshift.zeiterfassung.user; + +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.Locale; + +@Component +public class DateRangeFormatter { + + private final DateFormatter dateFormatter; + private final MessageSource messageSource; + + public DateRangeFormatter(DateFormatter dateFormatter, MessageSource messageSource) { + this.dateFormatter = dateFormatter; + this.messageSource = messageSource; + } + + public String toDateRangeString(LocalDate from, LocalDate to) { + + final MonthFormat firstMonthFormat = + from.getMonthValue() == to.getMonthValue() ? MonthFormat.NONE : MonthFormat.STRING; + + final YearFormat firstYearFormat = + from.getYear() == to.getYear() ? YearFormat.NONE : YearFormat.FULL; + + final String firstDateString = dateFormatter.formatDate(from, firstMonthFormat, firstYearFormat); + final String lastDateString = dateFormatter.formatDate(to, MonthFormat.STRING, YearFormat.FULL); + + return messageSource.getMessage("date-range", new Object[]{firstDateString, lastDateString}, locale()); + } + + private Locale locale() { + return LocaleContextHolder.getLocale(); + } +} diff --git a/src/main/java/de/focusshift/zeiterfassung/workingtime/PlannedWorkingHours.java b/src/main/java/de/focusshift/zeiterfassung/workingtime/PlannedWorkingHours.java index cfd31d8b9..63deaf245 100644 --- a/src/main/java/de/focusshift/zeiterfassung/workingtime/PlannedWorkingHours.java +++ b/src/main/java/de/focusshift/zeiterfassung/workingtime/PlannedWorkingHours.java @@ -1,9 +1,16 @@ package de.focusshift.zeiterfassung.workingtime; +import de.focusshift.zeiterfassung.absence.Absence; +import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; + import java.time.Duration; /** - * Defines a {@linkplain Duration} of planned working hours. + * Defines a {@linkplain Duration} of planned working hours. This does not include {@linkplain Absence}s. + * e.g. the employment contract of 40h a week. + * + *

+ * see {@linkplain ShouldWorkingHours} if you are interested in a value that includes sick days for instance. * * @param duration the exact duration. not rounded up to minutes. */ diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index d3cbc1f6f..f51a39f48 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -199,6 +199,9 @@ month.october.short=Okt month.november.short=Nov month.december.short=Dez +date-range={0} bis {1} +date-range.with-calendar-week=KW {0} | {1} + datepicker.today=Heute # duet-date-picker localization @@ -277,7 +280,6 @@ time-entry.form.delete.tooltip=Löschen time-entry.date.label=Datum time-entry.delete=Zeitslot wurde gelöscht. - report.page.meta.title=Zeiterfassung - Berichte report.view.select=Ansicht: @@ -313,11 +315,14 @@ report.csv.header.break=Pause report.time.pagination.navigation.aria-label=Bericht Seiten-Nummerierung +report.detail.summary.planned-working-hours=Geplante Arbeitszeit: +report.detail.summary.hours-worked=Geleistete Arbeitszeit: +report.detail.summary.hoursDelta.negative=Noch zu leistende Arbeitszeit: +report.detail.summary.hoursDelta.positive=Mehr geleistete Arbeitszeit: report.detail.section.heading=Zeiteinträge report.detail.month.section.title=Zeiteinträge für {0} report.detail.week.section.title=Kalenderwoche {2}: {0,date,short} bis {1,date,short} -report.detail.no-entries=Für diesen Tag wurden noch keine Zeiten erfasst. -report.detail.day.head={0}, {1} ({2}) +report.detail.day.head={0}, {1} report.detail.day.comment.label=Kommentar: report.detail.time={0} bis {1} diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index fd27be36f..cd29d0d81 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -199,6 +199,9 @@ month.october.short=Oct month.november.short=Nov month.december.short=Dec +date-range={0} to {1} +date-range.with-calendar-week=CW {0} | {1} + datepicker.today=Today # duet-date-picker localization @@ -312,11 +315,14 @@ report.csv.header.break=Break report.time.pagination.navigation.aria-label=Bericht Seiten-Nummerierung +report.detail.summary.planned-working-hours=Planned Working Hours: +report.detail.summary.hours-worked=Hours Worked: +report.detail.summary.hoursDelta.negative=Hours Remaining to Work: +report.detail.summary.hoursDelta.positive=Overtime Hours Worked: report.detail.section.heading=Time entries report.detail.month.section.title=Time entries of {0} report.detail.week.section.title=Calendar week {2}: {0,date,short} to {1,date,short} -report.detail.no-entries=No times have been recorded for this day yet. -report.detail.day.head={0}, {1} ({2}) +report.detail.day.head={0}, {1} report.detail.day.comment.label=Comment: report.detail.time={0} to {1} diff --git a/src/main/resources/templates/reports/user-report-month.html b/src/main/resources/templates/reports/user-report-month.html index fe0f64aa6..7d53eddab 100644 --- a/src/main/resources/templates/reports/user-report-month.html +++ b/src/main/resources/templates/reports/user-report-month.html @@ -72,9 +72,66 @@ >Dieser Monat - - Dezember 2021 - +

+
+
+ + Oktober 2024 + +

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

+

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

+
+
+
+
+
+
+
+
@@ -153,8 +209,7 @@ @@ -175,19 +230,18 @@ > - - - - avg - -
@@ -260,7 +289,7 @@ Zeiteinträge für Januar

-
    +
    1. diff --git a/src/main/resources/templates/reports/user-report-week.html b/src/main/resources/templates/reports/user-report-week.html index e7e2af4a5..c4315fc3e 100644 --- a/src/main/resources/templates/reports/user-report-week.html +++ b/src/main/resources/templates/reports/user-report-week.html @@ -65,16 +65,76 @@
      Diese Woche - - Dezember 2021, KW 42 - +
      +
      +
      + + KW 40 | 30. September bis 06. Oktober 2024 + +

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

      +

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

      +
      +
      +
      +
      +
      +
      +
      +
      @@ -193,22 +251,19 @@ th:id="${'mask-overtime-' + #strings.toLowerCase(day.dayOfWeekFull)}" > - - - - avg - -
      @@ -289,7 +321,6 @@ aria-hidden="true" >

      @@ -300,27 +331,65 @@ listLabelId=${'day-' + #strings.randomAlphanumeric(9)}, hasAbsences=${not #lists.isEmpty(day.absences)}" > -

      +

      - Zeiten -

      - + + Montag, 07.10.2024 + +

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

      - Für diesen Tag wurden noch keine Zeiten erfasst. + Geleistet: + + 41:58 + + (Soll: + 40:00 )

      -
      +
      + -
        +
        1. + -

          +
          -
          +
          diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java index f17361853..e84a8db0e 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportMonthControllerTest.java @@ -2,6 +2,7 @@ import de.focusshift.zeiterfassung.tenancy.user.EMailAddress; import de.focusshift.zeiterfassung.user.DateFormatterImpl; +import de.focusshift.zeiterfassung.user.DateRangeFormatter; import de.focusshift.zeiterfassung.user.UserId; import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.usermanagement.User; @@ -12,6 +13,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.MessageSource; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.test.web.servlet.ResultActions; @@ -23,11 +25,15 @@ import java.time.Year; import java.time.YearMonth; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import static java.time.ZoneOffset.UTC; import static java.util.Locale.GERMAN; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -45,12 +51,16 @@ class ReportMonthControllerTest { @Mock private ReportPermissionService reportPermissionService; + @Mock + private MessageSource messageSource; + private final Clock clock = Clock.systemUTC(); @BeforeEach void setUp() { final DateFormatterImpl dateFormatter = new DateFormatterImpl(); - final ReportControllerHelper helper = new ReportControllerHelper(reportPermissionService, dateFormatter); + final DateRangeFormatter dateRangeFormatter = new DateRangeFormatter(dateFormatter, messageSource); + final ReportControllerHelper helper = new ReportControllerHelper(reportPermissionService, dateFormatter, dateRangeFormatter); sut = new ReportMonthController(reportService, dateFormatter, helper, clock); } @@ -67,6 +77,8 @@ void ensureReportMonthForwardsToTodayMonthReport() throws Exception { @Test void ensureReportMonth() throws Exception { + when(messageSource.getMessage(anyString(), any(), any(Locale.class))).thenAnswer(returnsFirstArg()); + final UserId userId = new UserId("user-id"); final UserLocalId userLocalId = new UserLocalId(1L); final UserIdComposite userIdComposite = new UserIdComposite(userId, userLocalId); @@ -75,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) + ) + ) ) ); @@ -86,10 +124,11 @@ void ensureReportMonth() throws Exception { "Februar 2023", List.of( new GraphWeekDto( - "Januar 2023 KW 5", + 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), @@ -97,11 +136,95 @@ void ensureReportMonth() throws Exception { new GraphDayDto(false, "S", "Sonntag", "05.02.2023", 0d, 0d) ), 8d, - 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( @@ -364,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); } diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java index 277e97342..4ee856a67 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportServiceRawTest.java @@ -312,6 +312,10 @@ private static User anyUser() { final UserLocalId userLocalId = new UserLocalId(1L); final UserIdComposite userIdComposite = new UserIdComposite(userId, userLocalId); + return anyUser(userIdComposite); + } + + private static User anyUser(UserIdComposite userIdComposite) { return new User(userIdComposite, "Bruce", "Wayne", new EMailAddress(""), Set.of()); } diff --git a/src/test/java/de/focusshift/zeiterfassung/report/ReportWeekControllerTest.java b/src/test/java/de/focusshift/zeiterfassung/report/ReportWeekControllerTest.java index fd2fac43b..081f718e2 100644 --- a/src/test/java/de/focusshift/zeiterfassung/report/ReportWeekControllerTest.java +++ b/src/test/java/de/focusshift/zeiterfassung/report/ReportWeekControllerTest.java @@ -4,6 +4,7 @@ import de.focusshift.zeiterfassung.absence.DayLength; import de.focusshift.zeiterfassung.tenancy.user.EMailAddress; import de.focusshift.zeiterfassung.user.DateFormatterImpl; +import de.focusshift.zeiterfassung.user.DateRangeFormatter; import de.focusshift.zeiterfassung.user.UserId; import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.usermanagement.User; @@ -14,21 +15,22 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.MessageSource; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.threeten.extra.YearWeek; -import java.sql.Date; import java.time.Clock; -import java.time.Duration; import java.time.LocalDate; import java.time.LocalTime; import java.time.Year; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -36,6 +38,9 @@ import static de.focusshift.zeiterfassung.absence.AbsenceTypeCategory.HOLIDAY; import static java.time.ZoneOffset.UTC; import static java.util.Locale.GERMAN; +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -53,12 +58,16 @@ class ReportWeekControllerTest { @Mock private ReportPermissionService reportPermissionService; + @Mock + private MessageSource messageSource; + private final Clock clock = Clock.systemUTC(); @BeforeEach void setUp() { final DateFormatterImpl dateFormatter = new DateFormatterImpl(); - final ReportControllerHelper helper = new ReportControllerHelper(reportPermissionService, dateFormatter); + final DateRangeFormatter dateRangeFormatter = new DateRangeFormatter(dateFormatter, messageSource); + final ReportControllerHelper helper = new ReportControllerHelper(reportPermissionService, dateFormatter, dateRangeFormatter); sut = new ReportWeekController(reportService, helper, clock); } @@ -75,6 +84,8 @@ void ensureReportWeekForwardsToTodayWeekReport() throws Exception { @Test void ensureReportWeek() throws Exception { + when(messageSource.getMessage(anyString(), any(), any(Locale.class))).thenAnswer(returnsFirstArg()); + final UserId userId = new UserId("user-id"); final UserLocalId userLocalId = new UserLocalId(1L); final UserIdComposite userIdComposite = new UserIdComposite(userId, userLocalId); @@ -104,7 +115,8 @@ void ensureReportWeek() throws Exception { .thenReturn(reportWeek); final GraphWeekDto graphWeekDto = new GraphWeekDto( - "Januar 2023 KW 5", +5, + "date-range", List.of( new GraphDayDto(false, "M", "Montag", "30.01.2023", 8d, 8d), new GraphDayDto(false, "D", "Dienstag", "31.01.2023", 8d, 8d), @@ -115,7 +127,11 @@ void ensureReportWeek() throws Exception { new GraphDayDto(true, "S", "Sonntag", "05.02.2023", 0d, 0d) ), 8d, - 8d + "40:00", + "40:00", + "00:00", + false, + 100d ); perform( @@ -163,7 +179,7 @@ void ensureReportWeekWithAbsences() throws Exception { Date.from(ZonedDateTime.of(LocalDate.of(2023, 2,5), LocalTime.MIN, ZoneId.systemDefault()).toInstant()), 5, List.of( - new DetailDayDto(true, "F", "Freitag", "03.02.2023", Duration.ZERO, List.of(), + new DetailDayDto(true, "F", "Freitag", "03.02.2023", "00:00", "00:00", "00:00", false, List.of(), List.of( new DetailDayAbsenceDto( "Bruce Wayne", diff --git a/src/test/java/de/focusshift/zeiterfassung/timeentry/HasWorkedHoursRatioTest.java b/src/test/java/de/focusshift/zeiterfassung/timeentry/HasWorkedHoursRatioTest.java new file mode 100644 index 000000000..04b467627 --- /dev/null +++ b/src/test/java/de/focusshift/zeiterfassung/timeentry/HasWorkedHoursRatioTest.java @@ -0,0 +1,45 @@ +package de.focusshift.zeiterfassung.timeentry; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.Duration; + +import static java.math.RoundingMode.CEILING; +import static org.assertj.core.api.Assertions.assertThat; + +class HasWorkedHoursRatioTest { + + @Test + void ensureWorkedHoursRatioIsZeroWhenWorkedDurationIsZero() { + final HasWorkedHoursRatio sut = hasWorkedHoursRatio(WorkDuration.ZERO, ShouldWorkingHours.EIGHT); + assertThat(sut.workedHoursRatio()).isEqualTo(BigDecimal.ZERO); + } + + @Test + void ensureWorkedHoursRatioIsOneWhenShouldWorkingHoursIsZero() { + final HasWorkedHoursRatio sut = hasWorkedHoursRatio(WorkDuration.EIGHT, ShouldWorkingHours.ZERO); + assertThat(sut.workedHoursRatio()).isEqualTo(BigDecimal.ONE); + } + + @Test + void ensureWorkedHoursRatio() { + final HasWorkedHoursRatio sut = hasWorkedHoursRatio(new WorkDuration(Duration.ofHours(4)), ShouldWorkingHours.EIGHT); + assertThat(sut.workedHoursRatio()).isEqualTo(BigDecimal.valueOf(0.5).setScale(2, CEILING)); + } + + private HasWorkedHoursRatio hasWorkedHoursRatio(WorkDuration workDuration, ShouldWorkingHours shouldWorkingHours) { + return new HasWorkedHoursRatio() { + + @Override + public WorkDuration workDuration() { + return workDuration; + } + + @Override + public ShouldWorkingHours shouldWorkingHours() { + return shouldWorkingHours; + } + }; + } +}