Implement per-month employee working hours

Closes #76

Co-auther-by: Denis Natusch <denis.natusch@mailbox.tu-dresden.de>
This commit is contained in:
Simon Bruder 2023-12-05 15:21:22 +01:00 committed by Denis Natusch
parent e4d23d8e81
commit 580d3a6af6
No known key found for this signature in database
GPG key ID: 5E57BD8EDACFA985
9 changed files with 120 additions and 10 deletions

View file

@ -25,7 +25,14 @@ package catering.order {
class CustomOrder { class CustomOrder {
- start : LocalDateTime - start : LocalDateTime
- finish : LocalDateTime - finish : LocalDateTime
+ getDurationInSeconds(start:LocalDateTime,finish:LocalDateTime): long
+ getDurationInHours(start:LocalDateTime,finish:LocalDateTime): long
+ min(a:LocalDateTime,b:LocalDateTime): LocalDateTime
+ max(b:LocalDateTime,b:LocalDateTime): LocalDateTime
+ getDurationInSecondsDuringMonth(month:YearMonth): long
} }
CustomOrder ..> time.LocalDateTime
CustomOrder ..> time.YearMonth
enum EventType { enum EventType {
EVENT_CATERING EVENT_CATERING
@ -72,6 +79,7 @@ OrderController .....> catering.orderCatalog.OrderCatalogEntry
package time { package time {
class LocalDateTime class LocalDateTime
class DateTimeFormatter class DateTimeFormatter
class YearMonth
} }
@enduml @enduml

BIN
src/main/asciidoc/models/design/order.svg (Stored with Git LFS)

Binary file not shown.

View file

@ -47,8 +47,8 @@ package catering.staff {
class StaffController { class StaffController {
+ StaffController(staffRepository: StaffRepository) + StaffController(staffRepository: StaffRepository)
+ getStaff(model: Model): String + getStaff(model: Model, month:Optional<YearMonth>): String
+ getStaff(model: Model, form: StaffForm): String + getStaff(model: Model, form: StaffForm, month:Optional<YearMonth>): String
+ removeEmployee(employee: Employee, model: Model): String + removeEmployee(employee: Employee, model: Model): String
+ addEmployee(form:StaffForm, result:Errors, model: Model): String + addEmployee(form:StaffForm, result:Errors, model: Model): String
+ editEmployee(employee: Employee, model Model): String + editEmployee(employee: Employee, model Model): String
@ -70,6 +70,7 @@ package catering.staff {
+ findAll(): Streamable<Employee> + findAll(): Streamable<Employee>
+ delete(id: Long): void + delete(id: Long): void
+ getAvailableStaffByJob(job: JobType, start:LocalDateTime, finish:LocalDateTime): Set<Employee> + getAvailableStaffByJob(job: JobType, start:LocalDateTime, finish:LocalDateTime): Set<Employee>
+ getWorkingHourseByemployee(e: Employee,month: YearMonth): double
} }
StaffManagement --> StaffRepository : -staffRepository StaffManagement --> StaffRepository : -staffRepository
StaffManagement --> OrderManagement : -orderManagement StaffManagement --> OrderManagement : -orderManagement

View file

@ -11,6 +11,7 @@ import org.springframework.data.annotation.Id;
import javax.money.MonetaryAmount; import javax.money.MonetaryAmount;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -40,12 +41,37 @@ public class CustomOrder extends Order {
public CustomOrder() { } public CustomOrder() { }
private long getDurationInSeconds(LocalDateTime start, LocalDateTime finish) {
if (start.compareTo(finish) > 0) {
return 0;
}
return Duration.between(start, finish).getSeconds();
}
/** /**
* Helper function to get the amount of hours in between start and finish (analogous to CustomCart) * Helper function to get the amount of hours in between start and finish (analogous to CustomCart)
* @return hours between start and finish * @return hours between start and finish
*/ */
private long getDurationInHours(LocalDateTime start, LocalDateTime finish) {
return getDurationInSeconds(start, finish) / 3600;
}
public long getDurationInHours() { public long getDurationInHours() {
return Duration.between(start, finish).getSeconds() / 3600; return getDurationInHours(start, finish);
}
private static LocalDateTime min(LocalDateTime a, LocalDateTime b) {
return (a.compareTo(b)) < 0 ? a : b;
}
private static LocalDateTime max(LocalDateTime a, LocalDateTime b) {
return (a.compareTo(b)) > 0 ? a : b;
}
public long getDurationInSecondsDuringMonth(YearMonth month) {
LocalDateTime startInMonth = max(this.start, month.atDay(1).atStartOfDay());
LocalDateTime finishInMonth = min(this.finish, month.plusMonths(1).atDay(1).atStartOfDay());
return getDurationInSeconds(startInMonth, finishInMonth);
} }
/** /**

View file

@ -2,6 +2,9 @@ package catering.staff;
import static org.salespointframework.core.Currencies.EURO; import static org.salespointframework.core.Currencies.EURO;
import java.time.YearMonth;
import java.util.Optional;
import org.javamoney.moneta.Money; import org.javamoney.moneta.Money;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@ -26,13 +29,15 @@ public class StaffController {
@GetMapping("/staff") @GetMapping("/staff")
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
public String getStaff(Model model) { public String getStaff(Model model, @RequestParam Optional<YearMonth> month) {
return getStaff(model, new StaffForm()); return getStaff(model, new StaffForm(), month.orElseGet(YearMonth::now));
} }
public String getStaff(Model model, @Valid StaffForm form) { public String getStaff(Model model, @Valid StaffForm form, YearMonth month) {
model.addAttribute("staff", staffManagement.findAll()); model.addAttribute("staff", staffManagement.findAll());
model.addAttribute("form", form); model.addAttribute("form", form);
model.addAttribute("month", month);
model.addAttribute("management", staffManagement);
return "staff"; return "staff";
} }
@ -41,7 +46,7 @@ public class StaffController {
public String addEmployee(@Valid @ModelAttribute("form") StaffForm form, Errors result, Model model) { public String addEmployee(@Valid @ModelAttribute("form") StaffForm form, Errors result, Model model) {
form.validate(result); form.validate(result);
if (result.hasErrors()) { if (result.hasErrors()) {
return getStaff(model, form); return getStaff(model, form, YearMonth.now());
} }
staffManagement.save(new Employee(form.getName(), form.getJob(), Money.of(form.getWage(), EURO))); staffManagement.save(new Employee(form.getName(), form.getJob(), Money.of(form.getWage(), EURO)));
return "redirect:/staff"; return "redirect:/staff";

View file

@ -1,6 +1,7 @@
package catering.staff; package catering.staff;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -104,4 +105,10 @@ public class StaffManagement {
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
public double getWorkingHoursByEmployee(Employee e, YearMonth month) {
return orderManagement.findAll(Pageable.unpaged()).stream()
.filter(order -> order.getStaff().contains(e))
.map(order -> order.getDurationInSecondsDuringMonth(month))
.collect(Collectors.summingDouble(seconds -> (double) seconds / 3600));
}
} }

View file

@ -10,11 +10,19 @@
<div layout:fragment="content"> <div layout:fragment="content">
<div> <div>
<h2>Mitarbeiterdetails</h2> <h2>Mitarbeiterdetails</h2>
<form th:action="@{/staff}" method="get">
<div class="mb-3">
<label for="month">Monat:</label>
<input type="month" name="month" id="month" th:value="${month}" required>
</div>
<button class="btn btn-primary" type="submit">Zeitangabe aktualisieren</button>
</form>
<table class="table"> <table class="table">
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Beruf</th> <th>Beruf</th>
<th>Lohn</th> <th>Lohn</th>
<th>Arbeitszeit</th>
<th></th> <th></th>
<th></th> <th></th>
</tr> </tr>
@ -23,6 +31,7 @@
<td th:text="${employee.name}">Max</td> <td th:text="${employee.name}">Max</td>
<td th:text="${employee.job}">Koch</td> <td th:text="${employee.job}">Koch</td>
<td th:text="${employee.wage}"></td> <td th:text="${employee.wage}"></td>
<td th:with="month=${month}" th:text="${management.getWorkingHoursByEmployee(employee, month)}"></td>
<td> <td>
<a th:href="@{'/staff/edit/' + ${employee.id}}" <a th:href="@{'/staff/edit/' + ${employee.id}}"
><button class="btn btn-warning">Bearbeiten</button></a ><button class="btn btn-warning">Bearbeiten</button></a

View file

@ -72,6 +72,7 @@ public class OrderControllerIntegrationTests {
myUser = userAccountManagement.findByUsername("andi").get(); myUser = userAccountManagement.findByUsername("andi").get();
admin = userAccountManagement.findByUsername("admin").get(); admin = userAccountManagement.findByUsername("admin").get();
orderManagement.findAll(Pageable.unpaged()).forEach(orderManagement::delete);
if (orderManagement.findAll(Pageable.unpaged()).stream().findAny().isEmpty()) { if (orderManagement.findAll(Pageable.unpaged()).stream().findAny().isEmpty()) {
myCart = new CustomCart(OrderType.EVENT_CATERING, myCart = new CustomCart(OrderType.EVENT_CATERING,

View file

@ -3,6 +3,7 @@ package catering.staff;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.endsWith;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.salespointframework.core.Currencies.EURO; import static org.salespointframework.core.Currencies.EURO;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -11,19 +12,32 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.Set;
import org.javamoney.moneta.Money; import org.javamoney.moneta.Money;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.salespointframework.order.OrderManagement;
import org.salespointframework.useraccount.UserAccountManagement;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import catering.order.CustomCart;
import catering.order.CustomOrder;
import catering.order.OrderType;
import catering.users.User;
import catering.users.UserManagement;
@AutoConfigureMockMvc @AutoConfigureMockMvc
@SpringBootTest @SpringBootTest
class StaffControllerIntegrationTests { class StaffControllerIntegrationTests {
@ -38,13 +52,44 @@ class StaffControllerIntegrationTests {
@Autowired @Autowired
private StaffManagement staffManagement; private StaffManagement staffManagement;
@Autowired
private OrderManagement<CustomOrder> orderManagement;
@Autowired
private UserManagement userManagement;
@Autowired
private UserAccountManagement userAccountManagement;
Employee defaultEmployee; Employee defaultEmployee;
Long defaultEmployeeId; Long defaultEmployeeId;
User defaultCustomer;
Employee orderEmployee;
CustomOrder defaultStaffOrder;
CustomOrder createCustomOrder(LocalDateTime start, LocalDateTime end, Set<Employee> staff) {
CustomOrder order = orderManagement.save(new CustomOrder(defaultCustomer.getUserAccount().getId(),
new CustomCart(OrderType.EVENT_CATERING, start, end)));
staff.forEach(order::addEmployee);
return orderManagement.save(order);
}
@BeforeEach @BeforeEach
void setup() throws Exception { void setup() throws Exception {
orderManagement.findAll(Pageable.unpaged()).forEach(orderManagement::delete);
staffManagement.findAll().map(Employee::getId).forEach(staffManagement::delete);
defaultEmployee = staffManagement.save(new Employee("Dieter Baum", JobType.COOK, Money.of(0, EURO))); defaultEmployee = staffManagement.save(new Employee("Dieter Baum", JobType.COOK, Money.of(0, EURO)));
defaultEmployeeId = defaultEmployee.getId(); defaultEmployeeId = defaultEmployee.getId();
if (userAccountManagement.findByUsername("hemming").isEmpty()) {
userManagement.createCustomer("hemming", "Nürnberger Platz", "123", "Hemming Quark");
}
defaultCustomer = userManagement.getUserByName("hemming").get();
orderEmployee = staffManagement.save(new Employee("Tyler Baum", JobType.COOK, Money.of(10, EURO)));
defaultStaffOrder = createCustomOrder(LocalDateTime.of(2023, 12, 20, 10, 0),
LocalDateTime.of(2023, 12, 20, 21, 0), Set.of(orderEmployee));
} }
@AfterEach @AfterEach
@ -158,6 +203,14 @@ class StaffControllerIntegrationTests {
.containsExactly("Dieter Baum", JobType.COOK); .containsExactly("Dieter Baum", JobType.COOK);
} }
@Test
@WithMockUser(username = "admin", password = "admin", roles = "ADMIN")
void viewHoursesOfWork() throws Exception {
mvc.perform(get("/staff"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("11.0")));
}
@Test @Test
@WithMockUser(username = "dieter", password = "123", roles = "CUSTOMER") @WithMockUser(username = "dieter", password = "123", roles = "CUSTOMER")
void refuseCustomer() throws Exception { void refuseCustomer() throws Exception {