overtime calculation optimized
This commit is contained in:
@ -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'
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
Reference in New Issue
Block a user