Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add aggregated info of selected users to report view #906

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/main/css/reports.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -57,6 +59,7 @@ void addUserFilterModelAttributes(Model model, boolean allUsersSelected, List<Us
model.addAttribute("users", selectableUserDtos);
model.addAttribute("selectedUsers", selectableUserDtos.stream().filter(SelectableUserDto::selected).toList());
model.addAttribute("selectedUserIds", selectedUserLocalIds.stream().map(UserLocalId::value).toList());
model.addAttribute("selectedUsersById", selectableUserDtos.stream().collect(toMap(SelectableUserDto::id, identity())));
model.addAttribute("allUsersSelected", allUsersSelected);
model.addAttribute("userReportFilterUrl", userReportFilterUrl);
}
Expand Down
51 changes: 25 additions & 26 deletions src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import de.focusshift.zeiterfassung.timeentry.ShouldWorkingHours;
import de.focusshift.zeiterfassung.timeentry.WorkDuration;
import de.focusshift.zeiterfassung.user.UserIdComposite;
import de.focusshift.zeiterfassung.usermanagement.UserLocalId;
import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;

import java.time.LocalDate;
Expand All @@ -16,6 +15,8 @@
import java.util.function.Predicate;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toMap;

record ReportDay(
LocalDate date,
Map<UserIdComposite, PlannedWorkingHours> plannedWorkingHoursByUser,
Expand All @@ -32,9 +33,30 @@ public PlannedWorkingHours plannedWorkingHours() {
}

public ShouldWorkingHours shouldWorkingHours() {
return calculateShouldWorkingHours(detailDayAbsencesByUser.values().stream().flatMap(Collection::stream));
}

public Map<UserIdComposite, ShouldWorkingHours> shouldWorkingHoursByUser() {
return detailDayAbsencesByUser.entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> calculateShouldWorkingHours(e.getValue().stream())));
}

public WorkDuration workDuration() {

final Stream<ReportDayEntry> allReportDayEntries = reportDayEntriesByUser.values()
.stream()
.flatMap(Collection::stream);

return calculateWorkDurationFrom(allReportDayEntries);
}

public Map<UserIdComposite, WorkDuration> 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<ReportDayAbsence> absences) {
final double absenceDayLengthValue = absences
.map(ReportDayAbsence::absence)
.map(Absence::dayLength)
.map(DayLength::getValue)
Expand All @@ -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<ReportDayEntry> 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<UserIdComposite> predicate) {
final List<ReportDayEntry> reportDayEntries = findValueByFirstKeyMatch(reportDayEntriesByUser, predicate).orElse(List.of());
return calculateWorkDurationFrom(reportDayEntries.stream());
}

private WorkDuration calculateWorkDurationFrom(Stream<ReportDayEntry> reportDayEntries) {
return reportDayEntries
.map(ReportDayEntry::workDuration)
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/de/focusshift/zeiterfassung/report/ReportMonth.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,12 +23,38 @@ public PlannedWorkingHours plannedWorkingHours() {
.reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus);
}

public Map<UserIdComposite, PlannedWorkingHours> plannedWorkingHoursByUser() {
final HashMap<UserIdComposite, PlannedWorkingHours> 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<UserIdComposite, ShouldWorkingHours> shouldWorkingHoursByUser() {
final HashMap<UserIdComposite, ShouldWorkingHours> 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()
Expand All @@ -49,4 +78,17 @@ public WorkDuration workDuration() {
.map(ReportWeek::workDuration)
.reduce(WorkDuration.ZERO, WorkDuration::plus);
}

public Map<UserIdComposite, WorkDuration> workDurationByUser() {
final HashMap<UserIdComposite, WorkDuration> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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";
}
Expand Down Expand Up @@ -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<ReportSelectedUserDurationAggregationDto> toReportSelectedUserDurationAggregationDto(ReportMonth reportMonth) {

final Map<UserIdComposite, WorkDuration> workedByUser = reportMonth.workDurationByUser();
final Map<UserIdComposite, ShouldWorkingHours> shouldByUser = reportMonth.shouldWorkingHoursByUser();
final Map<UserIdComposite, PlannedWorkingHours> 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<DetailWeekDto> weeks = reportMonth.weeks()
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,12 +22,38 @@ public PlannedWorkingHours plannedWorkingHours() {
.reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus);
}

public Map<UserIdComposite, PlannedWorkingHours> plannedWorkingHoursByUser() {
final HashMap<UserIdComposite, PlannedWorkingHours> 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<UserIdComposite, ShouldWorkingHours> shouldWorkingHoursByUser() {
final HashMap<UserIdComposite, ShouldWorkingHours> 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()
Expand All @@ -47,6 +76,19 @@ public WorkDuration workDuration() {
.reduce(WorkDuration.ZERO, WorkDuration::plus);
}

public Map<UserIdComposite, WorkDuration> workDurationByUser() {
final HashMap<UserIdComposite, WorkDuration> 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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/javascript/components/avatar/avatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading