diff --git a/src/main/css/reports.css b/src/main/css/reports.css index ca4ce610..55f27637 100644 --- a/src/main/css/reports.css +++ b/src/main/css/reports.css @@ -28,6 +28,54 @@ display: none; } +.report-person-detail-table { + width: 100%; + @apply text-sm; + + th, + td { + @apply px-2; + @apply py-1; + &:last-of-type { + @apply pr-4; + } + } + + thead { + th { + @apply font-medium; + @apply text-sm; + } + } + + tbody { + tr { + .report-person-detail-table__avatar { + @apply text-blue-100; + @apply transition-colors; + } + td { + @apply bg-transparent; + @apply transition-colors; + &:first-of-type { + @apply rounded-l-2xl; + } + &:last-of-type { + @apply rounded-r-2xl; + } + } + &:hover { + td { + @apply bg-blue-50; + } + .report-person-detail-table__avatar { + @apply text-blue-200; + } + } + } + } +} + @screen xxs { .report-actions { grid-template-columns: auto; diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java index 591f87cd..a1f62213 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportControllerHelper.java @@ -26,7 +26,9 @@ import java.util.List; import java.util.Locale; +import static java.util.function.Function.identity; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; @Component class ReportControllerHelper { @@ -57,6 +59,7 @@ void addUserFilterModelAttributes(Model model, boolean allUsersSelected, List plannedWorkingHoursByUser, @@ -32,9 +33,30 @@ public PlannedWorkingHours plannedWorkingHours() { } public ShouldWorkingHours shouldWorkingHours() { + return calculateShouldWorkingHours(detailDayAbsencesByUser.values().stream().flatMap(Collection::stream)); + } + + public Map shouldWorkingHoursByUser() { + return detailDayAbsencesByUser.entrySet().stream() + .collect(toMap(Map.Entry::getKey, e -> calculateShouldWorkingHours(e.getValue().stream()))); + } + + public WorkDuration workDuration() { + + final Stream allReportDayEntries = reportDayEntriesByUser.values() + .stream() + .flatMap(Collection::stream); + + return calculateWorkDurationFrom(allReportDayEntries); + } + + public Map workDurationByUser() { + return reportDayEntriesByUser.entrySet().stream() + .collect(toMap(Map.Entry::getKey, entry -> calculateWorkDurationFrom(entry.getValue().stream()))); + } - final double absenceDayLengthValue = detailDayAbsencesByUser.values().stream() - .flatMap(Collection::stream) + private ShouldWorkingHours calculateShouldWorkingHours(Stream absences) { + final double absenceDayLengthValue = absences .map(ReportDayAbsence::absence) .map(Absence::dayLength) .map(DayLength::getValue) @@ -53,29 +75,6 @@ public ShouldWorkingHours shouldWorkingHours() { return new ShouldWorkingHours(plannedWorkingHours.duration()); } - public PlannedWorkingHours plannedWorkingHoursByUser(UserLocalId userLocalId) { - return findValueByFirstKeyMatch(plannedWorkingHoursByUser, userIdComposite -> userLocalId.equals(userIdComposite.localId())) - .orElse(PlannedWorkingHours.ZERO); - } - - public WorkDuration workDuration() { - - final Stream allReportDayEntries = reportDayEntriesByUser.values() - .stream() - .flatMap(Collection::stream); - - return calculateWorkDurationFrom(allReportDayEntries); - } - - public WorkDuration workDurationByUser(UserLocalId userLocalId) { - return workDurationByUserPredicate(userIdComposite -> userLocalId.equals(userIdComposite.localId())); - } - - private WorkDuration workDurationByUserPredicate(Predicate predicate) { - final List reportDayEntries = findValueByFirstKeyMatch(reportDayEntriesByUser, predicate).orElse(List.of()); - return calculateWorkDurationFrom(reportDayEntries.stream()); - } - private WorkDuration calculateWorkDurationFrom(Stream reportDayEntries) { return reportDayEntries .map(ReportDayEntry::workDuration) diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java index a5822812..e4853f5b 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java @@ -3,12 +3,15 @@ import de.focusshift.zeiterfassung.timeentry.HasWorkedHoursRatio; import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; +import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; import java.time.Duration; import java.time.YearMonth; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.util.function.Predicate.not; @@ -20,12 +23,38 @@ public PlannedWorkingHours plannedWorkingHours() { .reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus); } + public Map plannedWorkingHoursByUser() { + final HashMap plannedWorkingHoursByUser = new HashMap<>(); + + for (ReportWeek week : weeks) { + week.plannedWorkingHoursByUser().forEach((userIdComposite, plannedWorkingHours) -> { + final PlannedWorkingHours hours = plannedWorkingHoursByUser.getOrDefault(userIdComposite, PlannedWorkingHours.ZERO); + plannedWorkingHoursByUser.put(userIdComposite, hours.plus(plannedWorkingHours)); + }); + } + + return plannedWorkingHoursByUser; + } + public ShouldWorkingHours shouldWorkingHours() { return weeks.stream() .map(ReportWeek::shouldWorkingHours) .reduce(ShouldWorkingHours.ZERO, ShouldWorkingHours::plus); } + public Map shouldWorkingHoursByUser() { + final HashMap shouldWorkingHoursByUser = new HashMap<>(); + + for (ReportWeek week : weeks) { + week.shouldWorkingHoursByUser().forEach((userIdComposite, shouldWorkingHours) -> { + final ShouldWorkingHours hours = shouldWorkingHoursByUser.getOrDefault(userIdComposite, ShouldWorkingHours.ZERO); + shouldWorkingHoursByUser.put(userIdComposite, hours.plus(shouldWorkingHours)); + }); + } + + return shouldWorkingHoursByUser; + } + public WorkDuration averageDayWorkDuration() { final double averageMinutes = weeks.stream() @@ -49,4 +78,17 @@ public WorkDuration workDuration() { .map(ReportWeek::workDuration) .reduce(WorkDuration.ZERO, WorkDuration::plus); } + + public Map workDurationByUser() { + final HashMap byUser = new HashMap<>(); + + for (ReportWeek week : weeks) { + week.workDurationByUser().forEach((userIdComposite, dayDuration) -> { + final WorkDuration summedDuration = byUser.getOrDefault(userIdComposite, WorkDuration.ZERO); + byUser.put(userIdComposite, summedDuration.plus(dayDuration)); + }); + } + + return byUser; + } } diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java index d0956874..8c454761 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportMonthController.java @@ -5,7 +5,9 @@ import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; import de.focusshift.zeiterfassung.user.DateFormatter; +import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.usermanagement.UserLocalId; +import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +30,7 @@ import java.time.YearMonth; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import static java.lang.invoke.MethodHandles.lookup; @@ -114,6 +117,7 @@ public String monthlyUserReport( model.addAttribute("userReportCsvDownloadUrl", csvDownloadUrl); helper.addUserFilterModelAttributes(model, allUsersSelected, userLocalIds, String.format("/report/year/%d/month/%d", year, month)); + model.addAttribute("selectedUserDurationAggregation", toReportSelectedUserDurationAggregationDto(reportMonth)); return "reports/user-report"; } @@ -160,6 +164,23 @@ private GraphMonthDto toGraphMonthDto(ReportMonth reportMonth) { return new GraphMonthDto(yearMonth, graphWeekDtos, maxHoursWorked, workedWorkingHoursString, shouldWorkingHoursString, deltaHours, deltaDuration.isNegative(), weekRatio); } + record ReportSelectedUserDurationAggregationDto(Long userId, String delta, String worked, String should, String planned) {} + + private List toReportSelectedUserDurationAggregationDto(ReportMonth reportMonth) { + + final Map workedByUser = reportMonth.workDurationByUser(); + final Map shouldByUser = reportMonth.shouldWorkingHoursByUser(); + final Map plannedByUser = reportMonth.plannedWorkingHoursByUser(); + + return plannedByUser.keySet().stream().map(userIdComposite -> new ReportSelectedUserDurationAggregationDto( + userIdComposite.localId().value(), + durationToTimeString(Duration.ZERO), // TODO + durationToTimeString(workedByUser.get(userIdComposite).duration()), + durationToTimeString(shouldByUser.get(userIdComposite).duration()), + durationToTimeString(plannedByUser.get(userIdComposite).duration()) + )).toList(); + } + private DetailMonthDto toDetailMonthDto(ReportMonth reportMonth, Locale locale) { final List weeks = reportMonth.weeks() diff --git a/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java b/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java index d8af5c6e..aef4d3a6 100644 --- a/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java +++ b/src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java @@ -3,11 +3,14 @@ import de.focusshift.zeiterfassung.timeentry.HasWorkedHoursRatio; import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours; import de.focusshift.zeiterfassung.timeentry.WorkDuration; +import de.focusshift.zeiterfassung.user.UserIdComposite; import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours; import java.time.Duration; import java.time.LocalDate; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.util.function.Predicate.not; @@ -19,12 +22,38 @@ public PlannedWorkingHours plannedWorkingHours() { .reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus); } + public Map plannedWorkingHoursByUser() { + final HashMap plannedWorkingHoursByUser = new HashMap<>(); + + for (ReportDay reportDay : reportDays) { + reportDay.plannedWorkingHoursByUser().forEach((userIdComposite, plannedWorkingHours) -> { + final PlannedWorkingHours hours = plannedWorkingHoursByUser.getOrDefault(userIdComposite, PlannedWorkingHours.ZERO); + plannedWorkingHoursByUser.put(userIdComposite, hours.plus(plannedWorkingHours)); + }); + } + + return plannedWorkingHoursByUser; + } + public ShouldWorkingHours shouldWorkingHours() { return reportDays.stream() .map(ReportDay::shouldWorkingHours) .reduce(ShouldWorkingHours.ZERO, ShouldWorkingHours::plus); } + public Map shouldWorkingHoursByUser() { + final HashMap shouldWorkingHoursByUser = new HashMap<>(); + + for (ReportDay reportDay : reportDays) { + reportDay.shouldWorkingHoursByUser().forEach((userIdComposite, shouldWorkingHours) -> { + final ShouldWorkingHours hours = shouldWorkingHoursByUser.getOrDefault(userIdComposite, ShouldWorkingHours.ZERO); + shouldWorkingHoursByUser.put(userIdComposite, hours.plus(shouldWorkingHours)); + }); + } + + return shouldWorkingHoursByUser; + } + public WorkDuration averageDayWorkDuration() { final double averageMinutes = reportDays().stream() @@ -47,6 +76,19 @@ public WorkDuration workDuration() { .reduce(WorkDuration.ZERO, WorkDuration::plus); } + public Map workDurationByUser() { + final HashMap byUser = new HashMap<>(); + + for (ReportDay reportDay : reportDays) { + reportDay.workDurationByUser().forEach((userIdComposite, dayDuration) -> { + final WorkDuration summedDuration = byUser.getOrDefault(userIdComposite, WorkDuration.ZERO); + byUser.put(userIdComposite, summedDuration.plus(dayDuration)); + }); + } + + return byUser; + } + public LocalDate lastDateOfWeek() { return firstDateOfWeek.plusDays(6); } diff --git a/src/main/javascript/components/avatar/avatar.ts b/src/main/javascript/components/avatar/avatar.ts index 34395063..d0607e26 100644 --- a/src/main/javascript/components/avatar/avatar.ts +++ b/src/main/javascript/components/avatar/avatar.ts @@ -39,7 +39,7 @@ export class Avatar extends HTMLImageElement { const parent = this.parentElement; parent.replaceChild(t.content, this); - parent.querySelector("svg").classList.add(...clazzes); + parent.querySelector("svg").classList.add(...clazzes, "cursor-default"); } private addTooltip(altText: string) { diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index d19159dc..95d27414 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -317,6 +317,12 @@ report.csv.header.break=Pause report.time.pagination.navigation.aria-label=Bericht Seiten-Nummerierung +report.aggregated.table.head.user=Person +report.aggregated.table.head.delta=Abweichung +report.aggregated.table.head.worked=Geleistet +report.aggregated.table.head.should=Soll +report.aggregated.table.head.planned=Geplant + report.detail.summary.planned-working-hours=Geplante Arbeitszeit: report.detail.summary.hours-worked=Geleistete Arbeitszeit: report.detail.summary.hoursDelta.negative=Noch zu leistende Arbeitszeit: diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index c97faf88..9c87d6e0 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -317,6 +317,12 @@ report.csv.header.break=Break report.time.pagination.navigation.aria-label=Bericht Seiten-Nummerierung +report.aggregated.table.head.user=Person +report.aggregated.table.head.delta=Difference +report.aggregated.table.head.worked=Worked +report.aggregated.table.head.should=Should +report.aggregated.table.head.planned=Planned + 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: diff --git a/src/main/resources/templates/reports/user-report.html b/src/main/resources/templates/reports/user-report.html index fa2306b7..27becec3 100644 --- a/src/main/resources/templates/reports/user-report.html +++ b/src/main/resources/templates/reports/user-report.html @@ -60,6 +60,88 @@ >
+
+ + + + + + + + + + + + + + + + + + + +
+ Person + + Abweichung + + Geleistet + + Soll + + Geplant +
+
+ + + + Bruce Wayne +
+
+ - + + - + + - + + - +
+