Skip to content

Commit

Permalink
add global setting for federal-state / public-holidays (#543)
Browse files Browse the repository at this point in the history
closes #516 

Here are some things you should have thought about:

**Multi-Tenancy**
- [x] Extended new entities with `AbstractTenantAwareEntity`?
- [x] New entity added to `TenantAwareDatabaseConfiguration`?
- [x] 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 info@focus-shift.de 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 Apr 24, 2024
2 parents 0acda5a + 38fc0f6 commit a37567f
Show file tree
Hide file tree
Showing 54 changed files with 2,493 additions and 1,037 deletions.
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,
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

0 comments on commit a37567f

Please sign in to comment.