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 global setting for federal-state / public-holidays #543

Merged
merged 20 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Zeiterfassung is using user permissions from oidc claim `groups` for mapping pos
* `ZEITERFASSUNG_WORKING_TIME_EDIT_ALL`: Allowed to edit working time of all users
* `ZEITERFASSUNG_OVERTIME_ACCOUNT_EDIT_ALL`: Allowed to edit overtime account of all users
* `ZEITERFASSUNG_PERMISSIONS_EDIT_ALL`: Allowed to edit permissions of all users
* `ZEITERFASSUNG_WORKING_TIME_EDIT_GLOBAL`: Allowed to edit global (company-wide default) working time

If you're using Keycloak, this can be configured via a predefined OIDC client mapper with name `groups`.
Create both permissions as `Realm roles` and assign user to those roles.
Expand Down Expand Up @@ -298,11 +299,11 @@ users.

As a user of a tenant can log in via `http://localhost:8060/`:

| username | password | role |
|------------|----------|----------------------------------------------------------------------------------------------------------------|
| boss | secret | `view_reports_all`, `working_time_edit_all`, `overtime_account_edit_all`, `zeiterfassung_permissions_edit_all` |
| office | secret | `view_reports_all`, `working_time_edit_all`, `overtime_account_edit_all`, `zeiterfassung_permissions_edit_all` |
| user | secret | |
| username | password | role |
|------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| boss | secret | `view_reports_all`, `working_time_edit_all`, `overtime_account_edit_all`, `zeiterfassung_permissions_edit_all`, `zeiterfassung_working_time_edit_global` |
| office | secret | `view_reports_all`, `working_time_edit_all`, `overtime_account_edit_all`, `zeiterfassung_permissions_edit_all`, `zeiterfassung_working_time_edit_global` |
| user | secret | |


### git hooks (optional)
Expand Down
9 changes: 9 additions & 0 deletions docker/keycloak/export/zeiterfassung-realm-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@
"clientRole" : false,
"containerId" : "zeiterfassung-realm",
"attributes" : { }
}, {
"id" : "71923301-670f-48ae-a372-648d70e6d7cf",
"name" : "zeiterfassung_working_time_edit_global",
"description" : "",
"composite" : false,
"clientRole" : false,
"containerId" : "zeiterfassung-realm",
"attributes" : { }
}, {
"id" : "d0fcc3dd-7e1e-46fa-89ce-f7727e1a46f3",
"name" : "zeiterfassung_overtime_account_edit_all",
Expand Down Expand Up @@ -392,6 +400,7 @@
"realmRoles" : [
"zeiterfassung_view_report_all",
"zeiterfassung_working_time_edit_all",
"zeiterfassung_working_time_edit_global",
"zeiterfassung_overtime_account_edit_all",
"zeiterfassung_permissions_edit_all"
],
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/de/focusshift/zeiterfassung/CachedSupplier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package de.focusshift.zeiterfassung;

import java.util.function.Supplier;

public class CachedSupplier<T> implements Supplier<T> {

private T cachedValue;
private final Supplier<T> supplier;

public CachedSupplier(Supplier<T> supplier) {
this.supplier = supplier;
}

@Override
public T get() {
if (cachedValue == null) {
cachedValue = supplier.get();
}
return cachedValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public enum FederalState {

NONE("none"),
GLOBAL("global"),

GERMANY_BADEN_WUERTTEMBERG("de", "bw"),
GERMANY_BAYERN("de", "by"),
Expand Down Expand Up @@ -142,7 +143,7 @@ public String getCountry() {
*/
public static Map<String, List<FederalState>> federalStatesTypesByCountry() {
return Arrays.stream(values())
.filter(federalState -> federalState != NONE)
.filter(federalState -> federalState != NONE && federalState != GLOBAL)
.collect(groupingBy(FederalState::getCountry));
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package de.focusshift.zeiterfassung.publicholiday;

import de.focus_shift.jollyday.core.HolidayManager;
import de.focusshift.zeiterfassung.CachedSupplier;
import de.focusshift.zeiterfassung.settings.FederalStateSettingsService;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

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

@Service
class PublicHolidaysServiceImpl implements PublicHolidaysService {

private final Map<String, HolidayManager> holidayManagers;
private final FederalStateSettingsService federalStateSettingsService;

PublicHolidaysServiceImpl(Map<String, HolidayManager> holidayManagers) {
PublicHolidaysServiceImpl(Map<String, HolidayManager> holidayManagers, FederalStateSettingsService federalStateSettingsService) {
this.holidayManagers = holidayManagers;
this.federalStateSettingsService = federalStateSettingsService;
}

@Override
Expand All @@ -26,20 +31,27 @@ public Map<FederalState, PublicHolidayCalendar> getPublicHolidays(LocalDate from
final LocalDate to = toExclusive.minusDays(1);
final Map<FederalState, PublicHolidayCalendar> calendar = new EnumMap<>(FederalState.class);

final Supplier<FederalState> globalFederalStateSupplier =
new CachedSupplier<>(() -> federalStateSettingsService.getFederalStateSettings().federalState());

for (FederalState federalState : federalStates) {
final Map<LocalDate, List<PublicHoliday>> holidays;
if (federalState == FederalState.NONE) {
holidays = Map.of();
} else {
final HolidayManager holidayManager = holidayManagers.get(federalState.getCountry());
holidays = holidayManager.getHolidays(from, to, federalState.getCodes())
.stream()
.map(holiday -> new PublicHoliday(holiday.getDate(), holiday::getDescription))
.collect(groupingBy(PublicHoliday::date));
}
calendar.put(federalState, new PublicHolidayCalendar(federalState, holidays));
federalState = FederalState.GLOBAL.equals(federalState) ? globalFederalStateSupplier.get() : federalState;
calendar.put(federalState, new PublicHolidayCalendar(federalState, holidays(from, to, federalState)));
}

return calendar;
}

private Map<LocalDate, List<PublicHoliday>> holidays(LocalDate from, LocalDate to, FederalState federalState) {

if (FederalState.NONE.equals(federalState)) {
return Map.of();
}

final HolidayManager holidayManager = holidayManagers.get(federalState.getCountry());
return holidayManager.getHolidays(from, to, federalState.getCodes())
.stream()
.map(holiday -> new PublicHoliday(holiday.getDate(), holiday::getDescription))
.collect(groupingBy(PublicHoliday::date));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum SecurityRole {
ZEITERFASSUNG_USER,
ZEITERFASSUNG_VIEW_REPORT_ALL,
ZEITERFASSUNG_WORKING_TIME_EDIT_ALL,
ZEITERFASSUNG_WORKING_TIME_EDIT_GLOBAL,
bseber marked this conversation as resolved.
Show resolved Hide resolved
ZEITERFASSUNG_OVERTIME_ACCOUNT_EDIT_ALL,
ZEITERFASSUNG_PERMISSIONS_EDIT_ALL;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package de.focusshift.zeiterfassung.settings;

import de.focusshift.zeiterfassung.publicholiday.FederalState;
import de.focusshift.zeiterfassung.web.html.HtmlOptgroupDto;
import de.focusshift.zeiterfassung.web.html.HtmlOptionDto;
import de.focusshift.zeiterfassung.web.html.HtmlSelectDto;

import java.util.ArrayList;
import java.util.List;

public class FederalStateSelectDtoFactory {

private FederalStateSelectDtoFactory() {
//
}

public static HtmlSelectDto federalStateSelectDto(FederalState selectedFederalState) {
return federalStateSelectDto(selectedFederalState, false);
}

public static HtmlSelectDto federalStateSelectDto(FederalState selectedFederalState, boolean includeGlobalSettingElement) {

final ArrayList<HtmlOptgroupDto> countries = new ArrayList<>();

final List<HtmlOptionDto> generalOptions = new ArrayList<>();
if (includeGlobalSettingElement) {
final HtmlOptionDto globalOption = new HtmlOptionDto("federalState.GLOBAL", FederalState.GLOBAL.name(), FederalState.GLOBAL.equals(selectedFederalState));
generalOptions.add(globalOption);
}

final HtmlOptionDto noneOption = new HtmlOptionDto("federalState.NONE", FederalState.NONE.name(), FederalState.NONE.equals(selectedFederalState));
generalOptions.add(noneOption);

countries.add(new HtmlOptgroupDto("country.general", generalOptions));

FederalState.federalStatesTypesByCountry().forEach((country, federalStates) -> {
final List<HtmlOptionDto> options = federalStates.stream()
.map(federalState -> new HtmlOptionDto(federalStateMessageKey(federalState), federalState.name(), federalState.equals(selectedFederalState)))
.toList();
final HtmlOptgroupDto optgroup = new HtmlOptgroupDto("country." + country, options);
countries.add(optgroup);
});

return new HtmlSelectDto(countries);
}

public static String federalStateMessageKey(FederalState federalState) {
return "federalState." + federalState.name();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.focusshift.zeiterfassung.settings;

import de.focusshift.zeiterfassung.publicholiday.FederalState;

/**
* Global federal-state settings. Can be overridden for an individual person.
*
* @param federalState the default federal-state and public holiday regulations
* @param worksOnPublicHoliday whether persons are working on public holidays or not
*/
public record FederalStateSettings(FederalState federalState, boolean worksOnPublicHoliday) {

public static final FederalStateSettings DEFAULT = new FederalStateSettings(FederalState.NONE, false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.focusshift.zeiterfassung.settings;

import de.focusshift.zeiterfassung.publicholiday.FederalState;

record FederalStateSettingsDto(FederalState federalState, boolean worksOnPublicHoliday) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package de.focusshift.zeiterfassung.settings;

import de.focusshift.zeiterfassung.publicholiday.FederalState;
import de.focusshift.zeiterfassung.tenancy.tenant.AbstractTenantAwareEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;

import java.util.Objects;

import static jakarta.persistence.EnumType.STRING;

@Entity(name = "settings_federal_state")
public class FederalStateSettingsEntity extends AbstractTenantAwareEntity {

@Id
@Column(name = "id", unique = true, nullable = false, updatable = false)
@SequenceGenerator(name = "settings_federal_state_seq", sequenceName = "settings_federal_state_seq")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "settings_federal_state_seq")
protected Long id;

@Column(name = "federal_state")
@Enumerated(STRING)
private FederalState federalState;

@Column(name = "works_on_public_holiday")
private boolean worksOnPublicHoliday;

protected FederalStateSettingsEntity() {
super(null);
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public FederalState getFederalState() {
return federalState;
}

public void setFederalState(FederalState federalState) {
this.federalState = federalState;
}

public boolean isWorksOnPublicHoliday() {
return worksOnPublicHoliday;
}

public void setWorksOnPublicHoliday(boolean worksOnPublicHoliday) {
this.worksOnPublicHoliday = worksOnPublicHoliday;
}

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

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

@Override
public String toString() {
return "FederalStateSettingsEntity{" +
"id=" + id +
", federalState=" + federalState +
", worksOnPublicHoliday=" + worksOnPublicHoliday +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.focusshift.zeiterfassung.settings;

import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

interface FederalStateSettingsRepository extends CrudRepository<FederalStateSettingsEntity, Long> {

Optional<FederalStateSettingsEntity> findByTenantId(String tenantId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.focusshift.zeiterfassung.settings;

public interface FederalStateSettingsService {

FederalStateSettings getFederalStateSettings();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package de.focusshift.zeiterfassung.settings;

import de.focus_shift.launchpad.api.HasLaunchpad;
import de.focusshift.zeiterfassung.timeclock.HasTimeClock;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import static de.focusshift.zeiterfassung.settings.FederalStateSelectDtoFactory.federalStateSelectDto;

@Controller
@RequestMapping("/settings")
@PreAuthorize("hasAuthority('ZEITERFASSUNG_WORKING_TIME_EDIT_GLOBAL')")
class SettingsController implements HasLaunchpad, HasTimeClock {

private final SettingsService settingsService;

SettingsController(SettingsService settingsService) {
this.settingsService = settingsService;
}

@GetMapping
String getSettings() {
return "redirect:settings/federal-state";
}

@GetMapping("/federal-state")
String getFederalStateSettings(Model model) {

final FederalStateSettings settings = settingsService.getFederalStateSettings();
final FederalStateSettingsDto federalStateSettingsDto = toFederalStateSettingsDto(settings);

model.addAttribute("federalStateSettings", federalStateSettingsDto);
model.addAttribute("federalStateSelect", federalStateSelectDto(federalStateSettingsDto.federalState()));

return "settings/settings";
}

@PostMapping("/federal-state")
ModelAndView saveSettings(@ModelAttribute("federalStateSettings") FederalStateSettingsDto federalStateSettings, BindingResult result) {

if (result.hasErrors()) {
return new ModelAndView("settings/settings");
}

settingsService.updateFederalStateSettings(federalStateSettings.federalState(), federalStateSettings.worksOnPublicHoliday());

return new ModelAndView("redirect:/settings/federal-state");
}

private FederalStateSettingsDto toFederalStateSettingsDto(FederalStateSettings federalStateSettings) {
return new FederalStateSettingsDto(federalStateSettings.federalState(), federalStateSettings.worksOnPublicHoliday());
}
}
Loading