Skip to content

Commit

Permalink
add ratio bar and use should- and actual worked hours
Browse files Browse the repository at this point in the history
  • Loading branch information
bseber committed Oct 11, 2024
1 parent 2d19a9d commit 4658798
Show file tree
Hide file tree
Showing 19 changed files with 182 additions and 682 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

import java.util.List;

record GraphWeekDto(int calendarWeek, String dateRangeString, List<GraphDayDto> dayReports, Double maxHoursWorked) {
record GraphWeekDto(
int calendarWeek,
String dateRangeString,
List<GraphDayDto> dayReports,
Double maxHoursWorked,
String workedWorkingHours,
String shouldWorkingHours,
String hoursDelta,
boolean hoursDeltaNegative,
double hoursWorkedRatio
) {

public Double graphLegendMaxHour() {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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;
Expand All @@ -10,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;
Expand Down Expand Up @@ -76,7 +80,23 @@ GraphWeekDto toGraphWeekDto(ReportWeek reportWeek, Month monthPivot) {
.mapToDouble(value -> value)
.max().orElse(0.0);

return new GraphWeekDto(calendarWeek, dateRangeString, dayReports, maxHoursWorked);
final WorkDuration workDuration = reportWeek.workDuration();
final ShouldWorkingHours shouldWorkingHours = reportWeek.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 = 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) {
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/de/focusshift/zeiterfassung/report/ReportDay.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -29,6 +32,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,10 @@ public String monthlyUserReport(
final List<UserLocalId> userLocalIds = optionalUserIds.orElse(List.of()).stream().map(UserLocalId::new).toList();
final boolean allUsersSelected = optionalAllUsersSelected.isPresent();

final ReportSummary monthSummaryDto = getMonthSummary(principal, allUsersSelected, yearMonth, userLocalIds);
final ReportMonth reportMonth = getReportMonth(principal, allUsersSelected, yearMonth, userLocalIds);
final GraphMonthDto graphMonthDto = toGraphMonthDto(reportMonth);
final DetailMonthDto detailMonthDto = toDetailMonthDto(reportMonth, locale);

model.addAttribute("reportSummary", monthSummaryDto);
model.addAttribute("monthReport", graphMonthDto);
model.addAttribute("monthReportDetail", detailMonthDto);

Expand Down Expand Up @@ -115,21 +113,6 @@ public String monthlyUserReport(
return "reports/user-report";
}

private ReportSummary getMonthSummary(OidcUser principal, boolean allUsersSelected, YearMonth yearMonth, List<UserLocalId> userLocalIds) {

final ReportSummary monthSummary;

if (allUsersSelected) {
monthSummary = reportService.getMonthSummaryForAllUsers(yearMonth);
} else if (userLocalIds.isEmpty()) {
monthSummary = reportService.getMonthSummary(yearMonth, helper.principalToUserId(principal));
} else {
monthSummary = reportService.getMonthSummary(yearMonth, userLocalIds);
}

return monthSummary;
}

private ReportMonth getReportMonth(OidcUser principal, boolean allUsersSelected, YearMonth yearMonth, List<UserLocalId> userLocalIds) {

final ReportMonth reportMonth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,4 @@ interface ReportService {
ReportMonth getReportMonth(YearMonth yearMonth, List<UserLocalId> userLocalIds);

ReportMonth getReportMonthForAllUsers(YearMonth yearMonth);

ReportSummary getWeekSummary(Year reportYear, int week, UserId userId);

ReportSummary getWeekSummary(Year reportYear, int week, List<UserLocalId> userLocalIds);

ReportSummary getWeekSummaryForAllUsers(Year reportYear, int week);

ReportSummary getMonthSummary(YearMonth yearMonth, UserId userId);

ReportSummary getMonthSummary(YearMonth yearMonth, List<UserLocalId> userLocalIds);

ReportSummary getMonthSummaryForAllUsers(YearMonth yearMonth);
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,38 +96,6 @@ public ReportMonth getReportMonthForAllUsers(YearMonth yearMonth) {
return reportMonthForPermittedUserIds(yearMonth, permittedUserLocalIds);
}

@Override
public ReportSummary getWeekSummary(Year reportYear, int week, UserId userId) {
return reportServiceRaw.getWeekSummary(reportYear, week, userId);
}

@Override
public ReportSummary getWeekSummary(Year reportYear, int week, List<UserLocalId> permittedUserLocalIds) {
return reportServiceRaw.getWeekSummary(reportYear, week, permittedUserLocalIds);
}

@Override
public ReportSummary getWeekSummaryForAllUsers(Year reportYear, int week) {
final List<UserLocalId> permittedUserLocalIds = reportPermissionService.findAllPermittedUserLocalIdsForCurrentUser();
return reportServiceRaw.getWeekSummary(reportYear, week, permittedUserLocalIds);
}

@Override
public ReportSummary getMonthSummary(YearMonth yearMonth, UserId userId) {
return reportServiceRaw.getMonthSummary(yearMonth, userId);
}

@Override
public ReportSummary getMonthSummary(YearMonth yearMonth, List<UserLocalId> permittedUserLocalIds) {
return reportServiceRaw.getMonthSummary(yearMonth, permittedUserLocalIds);
}

@Override
public ReportSummary getMonthSummaryForAllUsers(YearMonth yearMonth) {
final List<UserLocalId> permittedUserLocalIds = reportPermissionService.findAllPermittedUserLocalIdsForCurrentUser();
return reportServiceRaw.getMonthSummary(yearMonth, permittedUserLocalIds);
}

private ReportWeek reportWeekForPermittedUserIds(Year year, int week, List<UserLocalId> permittedUserLocalIds) {
return reportServiceRaw.getReportWeek(year, week, permittedUserLocalIds);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
Expand Down Expand Up @@ -75,36 +74,6 @@ ReportWeek getReportWeek(Year year, int week, UserId userId) {
period -> workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), List.of(user.userLocalId())));
}

ReportSummary getWeekSummary(Year year, int week, UserId userId) {

final User user = userManagementService.findUserById(userId)
.orElseThrow(() -> new IllegalStateException("could not find user id=%s".formatted(userId)));

return getWeekSummary(year, week, List.of(user.userLocalId()));
}

// TODO: Implementation should be aligned to de.focusshift.zeiterfassung.timeentry.TimeEntryServiceImpl.getEntryWeekPage
ReportSummary getWeekSummary(Year year, int week, List<UserLocalId> userLocalIds) {

final LocalDate firstDateOfWeek = userDateService.firstDayOfWeek(year, week);
final Period period = new Period(firstDateOfWeek, firstDateOfWeek.plusWeeks(1));

final Map<UserIdComposite, WorkingTimeCalendar> workingTimeCalendarForUsers = workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), userLocalIds);

final Duration summedPlannedWorkingHours = workingTimeCalendarForUsers.values().stream()
.map(calendar -> calendar.plannedWorkingHours(period.from, period.toExclusive))
.reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus)
.duration();

final Duration summedHoursWorked = timeEntryService.getEntriesByUserLocalIds(period.from, period.toExclusive(), userLocalIds)
.values().stream()
.flatMap(Collection::stream)
.map(timeEntry -> timeEntry.workDuration().duration())
.reduce(Duration.ZERO, Duration::plus);

return new ReportSummary(summedPlannedWorkingHours, summedHoursWorked, summedHoursWorked.minus(summedPlannedWorkingHours));
}

ReportWeek getReportWeek(Year year, int week, List<UserLocalId> userLocalIds) {
return createReportWeek(year, week,
period -> timeEntryService.getEntriesByUserLocalIds(period.from(), period.toExclusive(), userLocalIds),
Expand All @@ -130,30 +99,6 @@ ReportMonth getReportMonth(YearMonth yearMonth, UserId userId) {
period -> workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), List.of(user.userLocalId())));
}

ReportSummary getMonthSummary(YearMonth yearMonth, UserId userId) {
final User user = userManagementService.findUserById(userId)
.orElseThrow(() -> new IllegalStateException("could not find user id=%s".formatted(userId)));
return getMonthSummary(yearMonth, List.of(user.userLocalId()));
}

ReportSummary getMonthSummary(YearMonth yearMonth, List<UserLocalId> userLocalIds) {

final Period period = new Period(yearMonth.atDay(1), yearMonth.atEndOfMonth().plusDays(1));

final Map<UserIdComposite, WorkingTimeCalendar> workingTimeCalendarForUsers = workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), userLocalIds);
final Duration summedPlannedWorkingHours = workingTimeCalendarForUsers.values().stream()
.map(calendar -> calendar.plannedWorkingHours(period.from, period.toExclusive()))
.reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus)
.duration();

final Duration hoursWorked = timeEntryService.getEntriesByUserLocalIds(period.from, period.toExclusive(), userLocalIds).values().stream()
.flatMap(Collection::stream)
.map(timeEntry -> timeEntry.workDuration().duration())
.reduce(Duration.ZERO, Duration::plus);

return new ReportSummary(summedPlannedWorkingHours, hoursWorked, hoursWorked.minus(summedPlannedWorkingHours));
}

ReportMonth getReportMonth(YearMonth yearMonth, List<UserLocalId> userLocalIds) {
return createReportMonth(yearMonth,
period -> timeEntryService.getEntriesByUserLocalIds(period.from(), period.toExclusive(), userLocalIds),
Expand Down

This file was deleted.

26 changes: 26 additions & 0 deletions src/main/java/de/focusshift/zeiterfassung/report/ReportWeek.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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.LocalDate;
import java.util.List;

import static java.math.RoundingMode.CEILING;
import static java.util.function.Predicate.not;

record ReportWeek(LocalDate firstDateOfWeek, List<ReportDay> reportDays) {
Expand All @@ -17,6 +20,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()
Expand All @@ -42,4 +51,21 @@ public WorkDuration workDuration() {
public LocalDate lastDateOfWeek() {
return firstDateOfWeek.plusDays(6);
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,10 @@ public String weeklyUserReport(
final List<UserLocalId> userLocalIds = optionalUserIds.orElse(List.of()).stream().map(UserLocalId::new).toList();
final boolean allUsersSelected = optionalAllUsersSelected.isPresent();

final ReportSummary weekSummaryDto = getWeekSummary(principal, reportYearWeek, allUsersSelected, reportYear, userLocalIds);
final ReportWeek reportWeek = getReportWeek(principal, reportYearWeek, allUsersSelected, reportYear, userLocalIds);
final GraphWeekDto graphWeekDto = helper.toGraphWeekDto(reportWeek, reportWeek.firstDateOfWeek().getMonth());
final DetailWeekDto detailWeekDto = helper.toDetailWeekDto(reportWeek, reportWeek.firstDateOfWeek().getMonth(), locale);

model.addAttribute("reportSummary", weekSummaryDto);
model.addAttribute("weekReport", graphWeekDto);
model.addAttribute("weekReportDetail", detailWeekDto);

Expand Down Expand Up @@ -113,21 +111,6 @@ public String weeklyUserReport(
return "reports/user-report";
}

private ReportSummary getWeekSummary(OidcUser principal, YearWeek reportYearWeek, boolean allUsersSelected, Year reportYear, List<UserLocalId> userLocalIds) {

final ReportSummary weekSummary;

if (allUsersSelected) {
weekSummary = reportService.getWeekSummaryForAllUsers(reportYear, reportYearWeek.getWeek());
} else if (userLocalIds.isEmpty()) {
weekSummary = reportService.getWeekSummary(reportYear, reportYearWeek.getWeek(), helper.principalToUserId(principal));
} else {
weekSummary = reportService.getWeekSummary(reportYear, reportYearWeek.getWeek(), userLocalIds);
}

return weekSummary;
}

private ReportWeek getReportWeek(OidcUser principal, YearWeek reportYearWeek, boolean allUsersSelected, Year reportYear, List<UserLocalId> userLocalIds) {

final ReportWeek reportWeek;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* (e.g. {@linkplain PlannedWorkingHours} 40h - {@linkplain Absence} 8h = ShouldWorkingHours 32h)
*
* @param duration the exact duration. not rounded up to minutes.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class DateRangeFormatter {
private final DateFormatter dateFormatter;
private final MessageSource messageSource;

DateRangeFormatter(DateFormatter dateFormatter, MessageSource messageSource) {
public DateRangeFormatter(DateFormatter dateFormatter, MessageSource messageSource) {
this.dateFormatter = dateFormatter;
this.messageSource = messageSource;
}
Expand Down
Loading

0 comments on commit 4658798

Please sign in to comment.