Skip to content

Commit

Permalink
[WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
bseber committed May 7, 2023
1 parent 0d8ba3d commit 1798149
Show file tree
Hide file tree
Showing 24 changed files with 492 additions and 114 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package de.focusshift.zeiterfassung.overtime;

import de.focusshift.zeiterfassung.timeentry.TimeEntryDuration;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.util.Objects;

public final class OvertimeDuration implements TimeEntryDuration {

public static OvertimeDuration ZERO = new OvertimeDuration(Duration.ZERO);

private final Duration value;

public OvertimeDuration(Duration value) {
this.value = value;
}

@Override
public Duration value() {
return value;
}

@Override
public Duration minutes() {
final long seconds = value.toSeconds();

return seconds % 60 == 0
? value
: Duration.ofMinutes(value.toMinutes() + 1);
}

@Override
public double hoursDoubleValue() {
final long minutes = minutes().toMinutes();
return minutesToHours(minutes);
}

public OvertimeDuration plus(Duration duration) {
return new OvertimeDuration(value.plus(duration));
}

public OvertimeDuration plus(OvertimeDuration overtimeDuration) {
return new OvertimeDuration(value.plus(overtimeDuration.value));
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OvertimeDuration that = (OvertimeDuration) o;
return Objects.equals(value, that.value);
}

@Override
public int hashCode() {
return Objects.hash(value);
}

@Override
public String toString() {
return "OvertimeDuration{" +
"value=" + value +
'}';
}

private static double minutesToHours(long minutes) {
return BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.CEILING).doubleValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.List;

interface OvertimeRepository extends JpaRepository<TimeEntryEntity, Long> {
Expand All @@ -20,4 +21,7 @@ interface OvertimeRepository extends JpaRepository<TimeEntryEntity, Long> {
*/
@Query("select t.end - t.start from TimeEntryEntity t where t.isBreak = :isBreak and t.owner = :userId and t.start < :dateExclusive")
List<Duration> getDurationsToDate(boolean isBreak, String userId, Instant dateExclusive);

@Query("select t.owner as userId, (t.end - t.start) as duration from TimeEntryEntity t where t.isBreak = :isBreak and t.owner in :userIds and t.start < :dateExclusive")
List<TimeEntryBreakEntityView> getOvertimeDurationsToDate(boolean isBreak, Collection<String> userIds, Instant dateExclusive);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.focusshift.zeiterfassung.overtime;

import de.focusshift.zeiterfassung.usermanagement.UserLocalId;

import java.time.LocalDate;
import java.util.Collection;
import java.util.Map;

public interface OvertimeService {

/**
* Get accumulated overtime for all given users until the given date, exclusive.
*
* @param date exclusive date
* @param userLocalIds users
* @return accumulated overtime till the given date grouped by user
*/
Map<UserLocalId, OvertimeDuration> accumulatedOvertimeToDate(LocalDate date, Collection<UserLocalId> userLocalIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package de.focusshift.zeiterfassung.overtime;

import de.focusshift.zeiterfassung.usermanagement.User;
import de.focusshift.zeiterfassung.usermanagement.UserLocalId;
import de.focusshift.zeiterfassung.usermanagement.UserManagementService;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static java.time.ZoneOffset.UTC;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;

@Service
class OvertimeServiceImpl implements OvertimeService {

private final OvertimeRepository overtimeRepository;
private final UserManagementService userManagementService;

OvertimeServiceImpl(OvertimeRepository overtimeRepository, UserManagementService userManagementService) {
this.overtimeRepository = overtimeRepository;
this.userManagementService = userManagementService;
}

@Override
public Map<UserLocalId, OvertimeDuration> accumulatedOvertimeToDate(LocalDate date, Collection<UserLocalId> userLocalIds) {

final Instant dateExclusive = date.atStartOfDay().toInstant(UTC);
final Map<String, UserLocalId> localIdById = userManagementService.findAllUsersByLocalIds(userLocalIds).stream()
.collect(toMap(user -> user.id().value(), User::localId));

final Map<String, List<TimeEntryBreakEntityView>> byUserIdValue = overtimeRepository.getOvertimeDurationsToDate(false, localIdById.keySet(), dateExclusive)
.stream()
.collect(groupingBy(TimeEntryBreakEntityView::getUserId));

return byUserIdValue.entrySet()
.stream()
.map(entry -> Map.entry(localIdById.get(entry.getKey()), new OvertimeDuration(toDuration(entry.getValue()))))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private Duration toDuration(List<TimeEntryBreakEntityView> views) {
return views.stream().map(TimeEntryBreakEntityView::getDuration).reduce(Duration.ZERO, Duration::plus);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.focusshift.zeiterfassung.overtime;

import java.time.Duration;

interface TimeEntryBreakEntityView {

String getUserId();

Duration getDuration();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.focusshift.zeiterfassung.report;

import de.focusshift.zeiterfassung.overtime.OvertimeDuration;
import de.focusshift.zeiterfassung.user.DateFormatter;
import de.focusshift.zeiterfassung.user.UserId;
import de.focusshift.zeiterfassung.usermanagement.User;
Expand All @@ -14,10 +15,21 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

@Component
class ReportControllerHelper {
Expand Down Expand Up @@ -101,6 +113,61 @@ DetailWeekDto toDetailWeekDto(ReportWeek reportWeek, Month monthPivot) {
return new DetailWeekDto(Date.from(firstOfWeek.toInstant()), Date.from(lastOfWeek.toInstant()), calendarWeek, dayReports);
}

ReportOvertimesDto reportOvertimesDto(ReportWeek reportWeek) {
// person | M | T | W | T | F | S | S
// -----------------------------------
// john | 1 | 2 | 2 | 3 | 4 | 4 | 4 <- `ReportOvertimeDto ( personName, overtimes )`
// jane | | | 2 | 3 | 4 | 4 | 4 <- `ReportOvertimeDto ( personName, overtimes )`

// build up `users` peace by peace. one person could have the first working day in the middle of the week (jane).
final Set<User> users = new HashSet<>();

// {john} -> [1, 2, 2, 3, 4, 4, 4]
// {jane} -> [empty, empty, 2, 3, 4, 4, 4]
final Map<User, List<Optional<OvertimeDuration>>> overtimeDurationsByUser = new HashMap<>();

// used to initiate the persons list of overtimes.
// jane will be seen first on the third reportDay. she initially needs a list of `[null, null]`.
int nrOfHandledDays = 0;

for (ReportDay reportDay : reportWeek.reportDays()) {

final Map<UserLocalId, User> userByLocalId = reportDay.reportDayEntries()
.stream()
.map(ReportDayEntry::user)
.distinct()
.collect(toMap(User::localId, identity()));

users.addAll(userByLocalId.values());

for (User user : users) {
// fill `overtimeDurationsByUser` map
final var durations = overtimeDurationsByUser.computeIfAbsent(user, prepareOvertimeDurationList(nrOfHandledDays));
durations.add(reportDay.accumulatedOvertimeToDateEndOfBusinessByUser(user.localId()));
}

nrOfHandledDays++;
}

final List<ReportOvertimeDto> overtimeDtos = users.stream()
.sorted(Comparator.comparing(User::fullName))
.map(user -> new ReportOvertimeDto(user.fullName(), overtimeDurationToDouble(overtimeDurationsByUser, user)))
.collect(toList());

return new ReportOvertimesDto(reportWeek.dateOfWeeks(), overtimeDtos);
}

private static Function<User, List<Optional<OvertimeDuration>>> prepareOvertimeDurationList(int nrOfHandledDays) {
return (unused) -> IntStream.range(0, nrOfHandledDays).mapToObj((unused2) -> Optional.<OvertimeDuration>empty()).collect(toList());
}

private static List<Double> overtimeDurationToDouble(Map<User, List<Optional<OvertimeDuration>>> overtimeDurationsByUser, User user) {
return overtimeDurationsByUser.get(user).stream()
.map(maybe -> maybe.orElse(null))
.map(overtimeDuration -> overtimeDuration == null ? null : overtimeDuration.hoursDoubleValue())
.collect(toList());
}

String createUrl(String prefix, boolean allUsersSelected, List<UserLocalId> selectedUserLocalIds) {
String url = prefix;

Expand Down
36 changes: 36 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,6 @@
package de.focusshift.zeiterfassung.report;

import de.focusshift.zeiterfassung.overtime.OvertimeDuration;
import de.focusshift.zeiterfassung.timeentry.PlannedWorkingHours;
import de.focusshift.zeiterfassung.timeentry.WorkDuration;
import de.focusshift.zeiterfassung.usermanagement.UserLocalId;
Expand All @@ -13,9 +14,13 @@
import java.util.function.Predicate;
import java.util.stream.Stream;

import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.toMap;

record ReportDay(
LocalDate date,
Map<UserLocalId, PlannedWorkingHours> plannedWorkingHoursByUser,
Map<UserLocalId, OvertimeDuration> accumulatedOvertimeToDateByUser,
Map<UserLocalId, List<ReportDayEntry>> reportDayEntriesByUser
) {

Expand All @@ -31,6 +36,37 @@ public PlannedWorkingHours plannedWorkingHoursByUser(UserLocalId userLocalId) {
return findValueByFirstKeyMatch(plannedWorkingHoursByUser, userLocalId::equals).orElse(PlannedWorkingHours.ZERO);
}

public Optional<OvertimeDuration> accumulatedOvertimeToDateByUser(UserLocalId userLocalId) {
return findValueByFirstKeyMatch(accumulatedOvertimeToDateByUser, userLocalId::equals);
}

public Optional<OvertimeDuration> accumulatedOvertimeToDateEndOfBusinessByUser(UserLocalId userLocalId) {
return accumulatedOvertimeToDateByUser(userLocalId).map(overtimeDuration -> overtimeDuration.plus(overtimeByUser(userLocalId)));
}

public Map<UserLocalId, OvertimeDuration> accumulatedOvertimeToDateEndOfBusinessByUser() {
// `accumulatedOvertimeToDateByUser` could not contain persons with timeEntries at this day.
// we need to iterate ALL persons that should have worked this day.
return plannedWorkingHoursByUser.keySet()
.stream()
.map(userLocalId -> Map.entry(userLocalId, accumulatedOvertimeToDateEndOfBusinessByUser(userLocalId).orElse(OvertimeDuration.ZERO)))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}

public OvertimeDuration overtimeByUser(UserLocalId userLocalId) {

final WorkDuration workDuration = reportDayEntriesByUser.getOrDefault(userLocalId, List.of())
.stream()
.filter(not(ReportDayEntry::isBreak))
.map(ReportDayEntry::workDuration)
.reduce(WorkDuration.ZERO, WorkDuration::plus);

final PlannedWorkingHours plannedWorkingHours = plannedWorkingHoursByUser.get(userLocalId);
final Duration delta = workDuration.value().minus(plannedWorkingHours.value());

return new OvertimeDuration(delta);
}

public WorkDuration workDuration() {

final Stream<ReportDayEntry> allReportDayEntries = reportDayEntriesByUser.values()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.focusshift.zeiterfassung.report;

import java.util.List;

record ReportOvertimeDto(String personName, List<Double> overtimes) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.focusshift.zeiterfassung.report;

import java.time.LocalDate;
import java.util.List;

record ReportOvertimesDto(List<LocalDate> dayOfWeeks, List<ReportOvertimeDto> overtimes) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private ReportWeek emptyReportWeek(Year year, int week) {

private ReportWeek emptyReportWeek(LocalDate startOfWeekDate) {
final List<ReportDay> reportDays = IntStream.rangeClosed(0, 6)
.mapToObj(daysToAdd -> new ReportDay(startOfWeekDate.plusDays(daysToAdd), Map.of(), Map.of()))
.mapToObj(daysToAdd -> new ReportDay(startOfWeekDate.plusDays(daysToAdd), Map.of(), Map.of(), Map.of()))
.toList();

return new ReportWeek(startOfWeekDate, reportDays);
Expand Down
Loading

0 comments on commit 1798149

Please sign in to comment.