overtime calculation optimized

This commit is contained in:
Jörg Henke
2024-01-04 20:35:26 +01:00
parent 742446e46e
commit 8be05b8afc
8 changed files with 264 additions and 30 deletions

View File

@ -23,7 +23,7 @@ repositories {
}
dependencies {
implementation 'de.jottyfan:timetrackjooq:20240103d'
implementation 'de.jottyfan:timetrackjooq:20240104b'
implementation 'org.apache.logging.log4j:log4j-api:latest.release'
implementation 'org.apache.logging.log4j:log4j-core:latest.release'

View File

@ -8,11 +8,17 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.done.model.DoneBean;
import de.jottyfan.timetrack.modules.done.model.DoneModel;
import de.jottyfan.timetrack.modules.done.model.OvertimeBean;
import de.jottyfan.timetrack.modules.done.model.SummaryBean;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -53,6 +59,7 @@ public class DoneController extends CommonController {
model.addAttribute("doneList", list);
model.addAttribute("sum", sumBean);
model.addAttribute("daysum", doneService.getDaysum(day, username));
model.addAttribute("overtimeBean", doneService.getOvertimeBean(username));
model.addAttribute("schedule", weekBean.toJson());
model.addAttribute("recentList", doneService.getListRecent(username, 10));
model.addAttribute("projectList", doneService.getProjects(false));
@ -63,6 +70,14 @@ public class DoneController extends CommonController {
model.addAttribute("favorites", doneService.getFavorites(username));
return "done/list";
}
@RolesAllowed("timetrack_user")
@PostMapping("/done/list")
public String getListForDate(Model model, @ModelAttribute("day") LocalDate day) {
DoneModel doneModel = new DoneModel();
doneModel.setDay(day);
return getList(model, doneModel);
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/abort/{day}")
@ -177,4 +192,12 @@ public class DoneController extends CommonController {
doneService.usefavorite(id);
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@PostMapping(value = "/done/overtime/update")
public String upsertOvertime(@ModelAttribute("overtimeBean") OvertimeBean bean) {
String username = provider.getName();
doneService.upsertOvertime(bean, username);
return "redirect:/done/list";
}
}

View File

@ -1,26 +1,35 @@
package de.jottyfan.timetrack.modules.done;
import static de.jottyfan.timetrack.db.done.Tables.T_OVERTIME;
import static de.jottyfan.timetrack.db.done.Tables.T_REQUIRED_WORKTIME;
import static de.jottyfan.timetrack.db.done.Tables.V_DAY;
import static de.jottyfan.timetrack.db.profile.Tables.T_LOGIN;
import java.sql.Time;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.DatePart;
import org.jooq.Field;
import org.jooq.InsertOnDuplicateStep;
import org.jooq.Record1;
import org.jooq.Record3;
import org.jooq.Record5;
import org.jooq.SelectConditionStep;
import org.jooq.SelectHavingStep;
import org.jooq.UpdateConditionStep;
import org.jooq.impl.DSL;
import org.jooq.types.YearToSecond;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.done.tables.records.TOvertimeRecord;
import de.jottyfan.timetrack.modules.done.model.DaysumBean;
import de.jottyfan.timetrack.modules.done.model.OvertimeBean;
/**
*
@ -62,29 +71,82 @@ public class DoneRepository {
bean.setBreaks(r.get(BREAKTIME));
YearToSecond dayOvertime = r.get(V_DAY.DAY_OVERTIME);
Duration dayOvertimeDuration = dayOvertime == null ? null : dayOvertime.toDuration();
String dayOvertimeString = dayOvertimeDuration == null ? null : String.format("%3d:%2d", dayOvertimeDuration.toHours(), Math.abs(dayOvertimeDuration.toMinutes() % 60));
bean.setDayOvertime(dayOvertimeString == null ? "?" : dayOvertimeString);
bean.setDayOvertime(dayOvertimeDuration == null ? null : Long.valueOf(dayOvertimeDuration.toMinutes()).intValue());
bean.setTotalOvertime(getOvertimeOf(day, login));
return bean;
}
}
private LocalTime getOvertimeOf(LocalDate day, String login) {
// using sql string here, because the DSL.sum function does not allow LocalTime, whilst the postgreSQL sum function does allow time calculations
String sqlRaw = """
select o.worktime_offset + sum(d.worktime - r.required) as overtime
from done.v_day d
inner join done.t_required_worktime r on r.day = d.day and r.fk_login = d.fk_login
inner join done.t_overtime o on o.fk_login = d.fk_login
inner join profile.t_login p on p.pk = d.fk_login
where o.impact < d.day
and d.day <= '?'
and p.login = '?'
group by d.fk_login, o.worktime_offset;
""";
String sql = sqlRaw.replaceFirst("\\?", day.format(DateTimeFormatter.ISO_DATE)).replaceFirst("\\?", login);
private Integer getOvertimeOf(LocalDate day, String login) {
Field<Integer> OVERTIME = DSL.field("overtime", Integer.class);
SelectHavingStep<Record1<Integer>> sql = jooq
// @formatter:off
.select(T_OVERTIME.OVERTIME_MINUTES.plus(DSL.sum(DSL.extract(V_DAY.WORKTIME, DatePart.MINUTE).minus(T_REQUIRED_WORKTIME.REQUIRED_MINUTES))).as(OVERTIME))
.from(V_DAY)
.innerJoin(T_REQUIRED_WORKTIME).on(T_REQUIRED_WORKTIME.DAY.eq(V_DAY.DAY).and(T_REQUIRED_WORKTIME.FK_LOGIN.eq(V_DAY.FK_LOGIN)))
.innerJoin(T_OVERTIME).on(T_OVERTIME.FK_LOGIN.eq(V_DAY.FK_LOGIN))
.innerJoin(T_LOGIN).on(T_LOGIN.PK.eq(V_DAY.FK_LOGIN))
.where(T_OVERTIME.IMPACT.ge(V_DAY.DAY.cast(LocalDateTime.class)))
.and(V_DAY.DAY.le(day))
.and(T_LOGIN.LOGIN.eq(login))
.groupBy(V_DAY.FK_LOGIN, T_OVERTIME.OVERTIME_MINUTES);
// @formatter:on
LOGGER.trace(sql);
Time time = (Time) jooq.fetchOne(sql).get(0);
return time.toLocalTime();
return sql.fetchOne(OVERTIME);
}
public OvertimeBean getOvertimeBean(String login) {
SelectConditionStep<Record3<Integer, LocalDateTime, Integer>> sql = jooq
// @formatter:off
.select(T_OVERTIME.PK_OVERTIME,
T_OVERTIME.IMPACT,
T_OVERTIME.OVERTIME_MINUTES)
.from(T_OVERTIME)
.innerJoin(T_LOGIN).on(T_LOGIN.PK.eq(T_OVERTIME.FK_LOGIN))
.where(T_LOGIN.LOGIN.eq(login));
// @formatter:on
LOGGER.trace(sql);
Record3<Integer, LocalDateTime, Integer> r = sql.fetchOne();
OvertimeBean bean = new OvertimeBean();
if (r == null) {
bean.setImpact(LocalDate.now());
bean.setOvertimeMinutes(0);
} else {
bean.setId(r.get(T_OVERTIME.PK_OVERTIME));
bean.setImpact(r.get(T_OVERTIME.IMPACT).toLocalDate());
bean.setOvertimeMinutes(r.get(T_OVERTIME.OVERTIME_MINUTES));
}
return bean;
}
public void upsertOvertime(Integer pkOvertime, String login, LocalDate impact, Integer overtimeMinutes) {
if (pkOvertime == null) {
InsertOnDuplicateStep<TOvertimeRecord> sql = jooq
// @formatter:off
.insertInto(T_OVERTIME,
T_OVERTIME.IMPACT,
T_OVERTIME.OVERTIME_MINUTES,
T_OVERTIME.FK_LOGIN)
.select(jooq
.select(DSL.val(impact == null ? null : impact.atStartOfDay()), DSL.val(overtimeMinutes), T_LOGIN.PK)
.from(T_LOGIN)
.where(T_LOGIN.LOGIN.eq(login)));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
UpdateConditionStep<TOvertimeRecord> sql = jooq
// @formatter:off
.update(T_OVERTIME)
.set(T_OVERTIME.IMPACT, impact == null ? null : impact.atStartOfDay())
.set(T_OVERTIME.OVERTIME_MINUTES, overtimeMinutes)
.where(T_OVERTIME.PK_OVERTIME.eq(pkOvertime))
.and(T_OVERTIME.FK_LOGIN.in(jooq
.select(T_LOGIN.PK)
.from(T_LOGIN)
.where(T_LOGIN.LOGIN.eq(login))));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
}

View File

@ -22,6 +22,7 @@ import de.jottyfan.timetrack.db.done.tables.records.VProjectRecord;
import de.jottyfan.timetrack.modules.done.model.DaysumBean;
import de.jottyfan.timetrack.modules.done.model.DoneBean;
import de.jottyfan.timetrack.modules.done.model.FavoriteBean;
import de.jottyfan.timetrack.modules.done.model.OvertimeBean;
import de.jottyfan.timetrack.modules.note.NoteService;
/**
@ -235,4 +236,12 @@ public class DoneService {
public DaysumBean getDaysum(LocalDate day, String login) {
return repository.getDaysum(day, login);
}
public OvertimeBean getOvertimeBean(String login) {
return repository.getOvertimeBean(login);
}
public void upsertOvertime(OvertimeBean bean, String username) {
repository.upsertOvertime(bean.getId(), username, bean.getImpact(), bean.getOvertimeMinutes());
}
}

View File

@ -15,8 +15,44 @@ public class DaysumBean implements Serializable {
private LocalTime daytimeUntil;
private LocalTime dayworktime;
private LocalTime breaks;
private String dayOvertime;
private LocalTime totalOvertime;
private Integer dayOvertime;
private Integer totalOvertime;
private String lz(Integer i) {
if (i < 10) {
return "0" + i;
} else {
return i.toString();
}
}
public String printTotalOvertime() {
StringBuilder buf = new StringBuilder();
if (totalOvertime == null) {
buf.append("?");
} else {
Boolean isNegative = totalOvertime < 0;
buf.append(isNegative ? "-" : "");
buf.append(Math.abs(totalOvertime) / 60);
buf.append(":");
buf.append(lz(Math.abs(totalOvertime) % 60));
}
return buf.toString();
}
public String printDayOvertime() {
StringBuilder buf = new StringBuilder();
if (dayOvertime == null) {
buf.append("?");
} else {
Boolean isNegative = dayOvertime < 0;
buf.append(isNegative ? "-" : "");
buf.append(Math.abs(dayOvertime) / 60);
buf.append(":");
buf.append(lz(Math.abs(dayOvertime) % 60));
}
return buf.toString();
}
/**
* @return the daytimeFrom
@ -77,28 +113,28 @@ public class DaysumBean implements Serializable {
/**
* @return the dayOvertime
*/
public String getDayOvertime() {
public Integer getDayOvertime() {
return dayOvertime;
}
/**
* @param dayOvertime the dayOvertime to set
*/
public void setDayOvertime(String dayOvertime) {
public void setDayOvertime(Integer dayOvertime) {
this.dayOvertime = dayOvertime;
}
/**
* @return the totalovertime
*/
public LocalTime getTotalOvertime() {
public Integer getTotalOvertime() {
return totalOvertime;
}
/**
* @param totalovertime the totalovertime to set
*/
public void setTotalOvertime(LocalTime totalOvertime) {
public void setTotalOvertime(Integer totalOvertime) {
this.totalOvertime = totalOvertime;
}

View File

@ -0,0 +1,62 @@
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
/**
*
* @author jotty
*
*/
public class OvertimeBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
@DateTimeFormat(pattern="yyyy-MM-dd")
private LocalDate impact;
private Integer overtimeMinutes;
/**
* @return the id
*/
public Integer getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Integer id) {
this.id = id;
}
/**
* @return the impact
*/
public LocalDate getImpact() {
return impact;
}
/**
* @param impact the impact to set
*/
public void setImpact(LocalDate impact) {
this.impact = impact;
}
/**
* @return the overtimeMinutes
*/
public Integer getOvertimeMinutes() {
return overtimeMinutes;
}
/**
* @param overtimeMinutes the overtimeMinutes to set
*/
public void setOvertimeMinutes(Integer overtimeMinutes) {
this.overtimeMinutes = overtimeMinutes;
}
}

View File

@ -310,6 +310,15 @@ body {
padding: 4px;
}
.emphpink {
font-weight: bolder;
color: #613583;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
}
.tab-pane-table {
background-color: white;
padding: 8px;

View File

@ -52,6 +52,8 @@
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_module">Modul</a></li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_job">Aufgabe</a></li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_billing">Abrechnung</a></li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_overtime">Überstunden</a></li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_slot">Slots</a></li>
</ul>
<div class="tabdivblurred tab-content">
<div id="div_list" class="tab-pane active tab-pane-table">
@ -104,8 +106,8 @@
<td>Ende: <span class="emphgreen" th:text="${#temporals.format(daysum.daytimeUntil, 'HH:mm')}" th:if="${daysum.daytimeUntil}"></span></td>
<td>Arbeitszeit total: <span class="emphblue" th:text="${#temporals.format(daysum.dayworktime, 'HH:mm')}" th:if="${daysum.dayworktime}"></span></td>
<td>Pausezeit total: <span class="emphorange" th:text="${#temporals.format(daysum.breaks, 'HH:mm')}" th:if="${daysum.breaks}"></span></td>
<td>Überstunden heute: <span class="emphred" th:text="${daysum.dayOvertime}"></span></td>
<td colspan="2">Überstunden total: <span class="emphred" th:text="${#temporals.format(daysum.totalOvertime, 'HH:mm')}" th:if="${daysum.totalOvertime}"></span></td>
<td>Überstunden heute: <span class="emphred" th:text="${daysum.printDayOvertime()}"></span></td>
<td colspan="2">Überstunden total: <span class="emphpink" th:text="${daysum.printTotalOvertime()}" th:if="${daysum.totalOvertime}"></span></td>
</tr>
<tr>
<td></td>
@ -213,6 +215,37 @@
</tbody>
</table>
</div>
<div id="div_overtime" class="tab-pane fade tab-pane-table">
<form th:action="@{/done/overtime/update}" method="post" th:object="${overtimeBean}">
<input type="hidden" th:field="*{id}" />
<div class="container">
<div class="row g-3">
<div class="col-sm-12">
<div class="alert alert-info">Hier werden die Überstunden einmalig angegeben. Dabei wird für einen bestimmten Tagesbeginn, an dem die Überstunden bekannt sind, der Wert gesetzt. Alle
nachfolgenden Zeiten werden bei der Anzeige der Überstunden während der Datenerfassung berücksichtigt und einberechnet.</div>
</div>
<div class="col-sm-3">Tagesbeginn</div>
<div class="col-sm-9">
<input type="date" th:field="*{impact}" class="form-control" />
</div>
<div class="col-sm-3">Überstunden (min)</div>
<div class="col-sm-9">
<input type="number" th:field="*{overtimeMinutes}" class="form-control" />
</div>
<div class="col-sm-3"></div>
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-primary">Übernehmen</button>
</div>
</div>
</div>
</form>
</div>
<div id="div_slot" class="tab-pane fade tab-pane-table">
<div class="alert alert-info">
Zur Berechnung der täglichen Überstunden müssen Slots angelegt werden, die definieren, an welchen Tagen wieviele Stunden zu arbeiten ist.
Urlaub und Arbeitsbefreiung können durch das Entfernen des jeweiligen Slots ermöglicht werden.
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {