Skip to content

Commit

Permalink
multiple working times (#484)
Browse files Browse the repository at this point in the history
closes #132

Here are some things you should have thought about:

**Multi-Tenancy**
- [ ] Extended new entities with `AbstractTenantAwareEntity`?
- [ ] New entity added to `TenantAwareDatabaseConfiguration`?
- [ ] Tested with `dev-multitenant` profile?

<!--

Thanks for contributing to the zeiterfassung.
Please review the following notes before submitting you pull request.

Please look for other issues or pull requests which already work on this
topic. Is somebody already on it? Do you need to synchronize?

# Security Vulnerabilities

🛑 STOP! 🛑 If your contribution fixes a security vulnerability, please do
not submit it.
Instead, please write an E-Mail to [email protected] with all the
information
to recreate the security vulnerability.

# Describing Your Changes

If, having reviewed the notes above, you're ready to submit your pull
request, please
provide a brief description of the proposed changes.

If they:
🐞 fix a bug, please describe the broken behaviour and how the changes
fix it.
    Please label with 'type: bug' and 'status: new'
    
🎁 make an enhancement, please describe the new functionality and why you
believe it's useful.
    Please label with 'type: enhancement' and 'status: new'
 
If your pull request relates to any existing issues,
please reference them by using the issue number prefixed with #.

-->
  • Loading branch information
derTobsch authored Dec 13, 2023
2 parents 726c2fa + 33b9076 commit 95839a5
Show file tree
Hide file tree
Showing 78 changed files with 3,961 additions and 2,327 deletions.
59 changes: 59 additions & 0 deletions src/main/css/2-components.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,62 @@
duet-date-picker.edited ~ label > span {
color: theme("colors.blue.600");
}

.button-primary {
@apply whitespace-nowrap;
@apply bg-blue-700;
@apply border;
@apply border-blue-700;
@apply text-white;
@apply font-medium;
@apply rounded;
@apply px-4;
@apply py-2;
@apply flex;
@apply transition-colors;
@apply hover:bg-blue-600;
@apply sm:justify-start;
}

.button-primary-icon {
@apply items-center;
@apply gap-2;
@apply justify-center;
}

.button-secondary {
@apply whitespace-nowrap;
@apply border;
@apply border-blue-700;
@apply text-blue-700;
@apply bg-white;
@apply font-medium;
@apply rounded;
@apply text-center;
@apply px-4;
@apply py-2;
@apply sm:text-left;
}

.button-secondary-icon {
@apply flex;
@apply items-center;
@apply gap-2;
}

.button-secondary--subtle {
@apply border-gray-400;
@apply text-gray-500;
@apply font-normal;
}

.button-secondary.button-secondary--narrow {
@apply py-1;
@apply px-4;
}

.button-secondary-icon.button-secondary--narrow {
@apply py-1;
@apply pl-3;
@apply pr-4;
}
58 changes: 58 additions & 0 deletions src/main/java/de/focusshift/zeiterfassung/DateRange.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package de.focusshift.zeiterfassung;

import java.time.LocalDate;
import java.util.Iterator;
import java.util.NoSuchElementException;

import static org.springframework.util.Assert.isTrue;
import static org.springframework.util.Assert.notNull;

/**
* Represents an immutable date range.
* <p>
* A date range represents a period of time between two LocalDates.
* Date range are inclusive of the start and the end date.
* The end date is always greater than or equal to the start date.
* <p>
*/
public record DateRange(LocalDate startDate, LocalDate endDate) implements Iterable<LocalDate> {

public DateRange {
notNull(startDate, "expected startDate not to be null");
notNull(endDate, "expected endDate not to be null");
isTrue(!startDate.isAfter(endDate), "expected startDate not to be after endDate");
}

@Override
public Iterator<LocalDate> iterator() {
return new DateRangeIterator(startDate, endDate);
}

private static final class DateRangeIterator implements Iterator<LocalDate> {

private final LocalDate endDate;
private LocalDate cursor;

DateRangeIterator(LocalDate startDate, LocalDate endDate) {
this.cursor = startDate;
this.endDate = endDate;
}

@Override
public boolean hasNext() {
return cursor.isBefore(endDate) || cursor.isEqual(endDate);
}

@Override
public LocalDate next() {

if (cursor.isAfter(endDate)) {
throw new NoSuchElementException("next date is after endDate which is not in range anymore.");
}

final LocalDate current = cursor;
cursor = cursor.plusDays(1);
return current;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package de.focusshift.zeiterfassung.report;

import de.focusshift.zeiterfassung.timeentry.PlannedWorkingHours;
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;
import java.util.Collection;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.focusshift.zeiterfassung.report;

import de.focusshift.zeiterfassung.timeentry.PlannedWorkingHours;
import de.focusshift.zeiterfassung.timeentry.WorkDuration;
import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;

import java.time.Duration;
import java.time.YearMonth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import de.focusshift.zeiterfassung.absence.Absence;
import de.focusshift.zeiterfassung.absence.AbsenceService;
import de.focusshift.zeiterfassung.absence.DayLength;
import de.focusshift.zeiterfassung.timeentry.PlannedWorkingHours;
import de.focusshift.zeiterfassung.timeentry.TimeEntry;
import de.focusshift.zeiterfassung.timeentry.TimeEntryService;
import de.focusshift.zeiterfassung.user.UserDateService;
Expand All @@ -12,8 +11,9 @@
import de.focusshift.zeiterfassung.usermanagement.User;
import de.focusshift.zeiterfassung.usermanagement.UserLocalId;
import de.focusshift.zeiterfassung.usermanagement.UserManagementService;
import de.focusshift.zeiterfassung.usermanagement.WorkingTimeCalendar;
import de.focusshift.zeiterfassung.usermanagement.WorkingTimeCalendarService;
import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;
import de.focusshift.zeiterfassung.workingtime.WorkingTimeCalendar;
import de.focusshift.zeiterfassung.workingtime.WorkingTimeCalendarService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -71,21 +71,21 @@ ReportWeek getReportWeek(Year year, int week, UserId userId) {
return createReportWeek(year, week,
period -> Map.of(user.userIdComposite(), timeEntryService.getEntries(period.from(), period.toExclusive(), userId)),
period -> Map.of(user.userIdComposite(), absenceService.getAbsencesByUserId(userId, period.from(), period.toExclusive())),
period -> workingTimeCalendarService.getWorkingTimes(period.from(), period.toExclusive(), List.of(user.userLocalId())));
period -> workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), List.of(user.userLocalId())));
}

ReportWeek getReportWeek(Year year, int week, List<UserLocalId> userLocalIds) {
return createReportWeek(year, week,
period -> timeEntryService.getEntriesByUserLocalIds(period.from(), period.toExclusive(), userLocalIds),
period -> absenceService.getAbsencesByUserIds(userLocalIds, period.from(), period.toExclusive()),
period -> workingTimeCalendarService.getWorkingTimes(period.from(), period.toExclusive(), userLocalIds));
period -> workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), userLocalIds));
}

ReportWeek getReportWeekForAllUsers(Year year, int week) {
return createReportWeek(year, week,
period -> timeEntryService.getEntriesForAllUsers(period.from(), period.toExclusive()),
period -> absenceService.getAbsencesForAllUsers(period.from(), period.toExclusive()),
period -> workingTimeCalendarService.getWorkingTimes(period.from(), period.toExclusive()));
period -> workingTimeCalendarService.getWorkingTimeCalendarForAllUsers(period.from(), period.toExclusive()));
}

ReportMonth getReportMonth(YearMonth yearMonth, UserId userId) {
Expand All @@ -96,21 +96,21 @@ ReportMonth getReportMonth(YearMonth yearMonth, UserId userId) {
return createReportMonth(yearMonth,
period -> timeEntryService.getEntriesByUserLocalIds(period.from(), period.toExclusive(), List.of(user.userLocalId())),
period -> Map.of(user.userIdComposite(), absenceService.getAbsencesByUserId(userId, period.from(), period.toExclusive())),
period -> workingTimeCalendarService.getWorkingTimes(period.from(), period.toExclusive(), List.of(user.userLocalId())));
period -> workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), List.of(user.userLocalId())));
}

ReportMonth getReportMonth(YearMonth yearMonth, List<UserLocalId> userLocalIds) {
return createReportMonth(yearMonth,
period -> timeEntryService.getEntriesByUserLocalIds(period.from(), period.toExclusive(), userLocalIds),
period -> absenceService.getAbsencesByUserIds(userLocalIds, period.from(), period.toExclusive()),
period -> workingTimeCalendarService.getWorkingTimes(period.from(), period.toExclusive(), userLocalIds));
period -> workingTimeCalendarService.getWorkingTimeCalendarForUsers(period.from(), period.toExclusive(), userLocalIds));
}

ReportMonth getReportMonthForAllUsers(YearMonth yearMonth) {
return createReportMonth(yearMonth,
period -> timeEntryService.getEntriesForAllUsers(period.from(), period.toExclusive()),
period -> absenceService.getAbsencesForAllUsers(period.from(), period.toExclusive()),
period -> workingTimeCalendarService.getWorkingTimes(period.from(), period.toExclusive()));
period -> workingTimeCalendarService.getWorkingTimeCalendarForAllUsers(period.from(), period.toExclusive()));
}

private ReportWeek createReportWeek(Year year, int week,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.focusshift.zeiterfassung.report;

import de.focusshift.zeiterfassung.timeentry.PlannedWorkingHours;
import de.focusshift.zeiterfassung.timeentry.WorkDuration;
import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;

import java.time.Duration;
import java.time.LocalDate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import de.focusshift.zeiterfassung.timeclock.TimeClockEntity;
import de.focusshift.zeiterfassung.timeentry.TimeEntryEntity;
import de.focusshift.zeiterfassung.usermanagement.OvertimeAccountEntity;
import de.focusshift.zeiterfassung.usermanagement.WorkingTimeEntity;
import de.focusshift.zeiterfassung.workingtime.WorkingTimeEntity;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.workingtime.ZeitDuration;

import java.time.Duration;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.workingtime.ZeitDuration;

import java.time.Duration;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.user.UserIdComposite;
import de.focusshift.zeiterfassung.workingtime.ZeitDuration;

import java.time.Duration;
import java.time.ZonedDateTime;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.absence.Absence;
import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;

import java.math.BigDecimal;
import java.time.Duration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import de.focusshift.zeiterfassung.usermanagement.User;
import de.focusshift.zeiterfassung.usermanagement.UserLocalId;
import de.focusshift.zeiterfassung.usermanagement.UserManagementService;
import de.focusshift.zeiterfassung.usermanagement.WorkingTimeService;
import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;
import de.focusshift.zeiterfassung.workingtime.WorkingTimeCalendar;
import de.focusshift.zeiterfassung.workingtime.WorkingTimeCalendarService;
import jakarta.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -51,20 +53,20 @@ class TimeEntryServiceImpl implements TimeEntryService {

private final TimeEntryRepository timeEntryRepository;
private final UserManagementService userManagementService;
private final WorkingTimeService workingTimeService;
private final WorkingTimeCalendarService workingTimeCalendarService;
private final UserDateService userDateService;
private final UserSettingsProvider userSettingsProvider;
private final AbsenceService absenceService;
private final Clock clock;

@Autowired
TimeEntryServiceImpl(TimeEntryRepository timeEntryRepository, UserManagementService userManagementService,
WorkingTimeService workingTimeService, UserDateService userDateService,
WorkingTimeCalendarService workingTimeCalendarService, UserDateService userDateService,
UserSettingsProvider userSettingsProvider, AbsenceService absenceService, Clock clock) {

this.timeEntryRepository = timeEntryRepository;
this.userManagementService = userManagementService;
this.workingTimeService = workingTimeService;
this.workingTimeCalendarService = workingTimeCalendarService;
this.userDateService = userDateService;
this.userSettingsProvider = userSettingsProvider;
this.absenceService = absenceService;
Expand Down Expand Up @@ -160,13 +162,14 @@ public TimeEntryWeekPage getEntryWeekPage(UserId userId, int year, int weekOfYea

// TODO refactor getEntryWeekPage to accept UserLocalId to replace userManagementService call
final UserLocalId userLocalId = user.userLocalId();
final Map<LocalDate, PlannedWorkingHours> plannedByDate = workingTimeService.getWorkingHoursByUserAndYearWeek(userLocalId, Year.of(year), weekOfYear);

final PlannedWorkingHours weekPlannedHours = plannedByDate.values()
.stream()
.reduce(PlannedWorkingHours.ZERO, PlannedWorkingHours::plus);
final WorkingTimeCalendar workingTimeCalendar = workingTimeCalendarService
.getWorkingTimeCalender(fromLocalDate, toLocalDateExclusive, userLocalId);

final List<TimeEntryDay> daysOfWeek = createTimeEntryDays(fromLocalDate, toLocalDateExclusive, timeEntriesByDate, absencesByDate, workingTimeCalendar);

final List<TimeEntryDay> daysOfWeek = createTimeEntryDays(fromLocalDate, toLocalDateExclusive, timeEntriesByDate, absencesByDate, plannedByDate);
final PlannedWorkingHours weekPlannedHours = workingTimeCalendar
.plannedWorkingHours(fromLocalDate, toLocalDateExclusive);

final TimeEntryWeek timeEntryWeek = new TimeEntryWeek(fromLocalDate, weekPlannedHours, daysOfWeek);
final long totalTimeEntries = timeEntryRepository.countAllByOwner(userId.value());
Expand Down Expand Up @@ -207,19 +210,23 @@ public TimeEntry updateTimeEntry(TimeEntryId id, String comment, @Nullable Zoned
private static List<TimeEntryDay> createTimeEntryDays(LocalDate from, LocalDate toExclusive,
Map<LocalDate, List<TimeEntry>> timeEntriesByDate,
Map<LocalDate, List<Absence>> absencesByDate,
Map<LocalDate, PlannedWorkingHours> plannedByDate) {
WorkingTimeCalendar workingTimeCalendar) {

final List<TimeEntryDay> timeEntryDays = new ArrayList<>();

// iterate from end to start -> last entry should be on top of the list (the first element)
LocalDate date = toExclusive.minusDays(1);

while (date.isEqual(from) || date.isAfter(from)) {
final PlannedWorkingHours plannedWorkingHours = plannedByDate.get(date);

final PlannedWorkingHours plannedWorkingHours = workingTimeCalendar.plannedWorkingHours(date)
.orElseThrow(() -> new IllegalStateException("expected plannedWorkingHours to exist in calendar."));

final List<TimeEntry> timeEntries = timeEntriesByDate.getOrDefault(date, List.of());
final List<Absence> absences = absencesByDate.getOrDefault(date, List.of());
final ShouldWorkingHours shouldWorkingHours = dayShouldHoursWorked(plannedWorkingHours, absences);
timeEntryDays.add(new TimeEntryDay(date, plannedWorkingHours, shouldWorkingHours, timeEntries, absences));

date = date.minusDays(1);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.workingtime.PlannedWorkingHours;
import org.threeten.extra.YearWeek;

import java.math.BigDecimal;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package de.focusshift.zeiterfassung.timeentry;

import de.focusshift.zeiterfassung.workingtime.ZeitDuration;

import java.time.Duration;

/**
Expand Down
Loading

0 comments on commit 95839a5

Please sign in to comment.