From 8be05b8afc2fc06d9dac788f9599dad229ef957f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Henke?= Date: Thu, 4 Jan 2024 20:35:26 +0100 Subject: [PATCH] overtime calculation optimized --- build.gradle | 2 +- .../modules/done/DoneController.java | 25 ++++- .../modules/done/DoneRepository.java | 102 ++++++++++++++---- .../timetrack/modules/done/DoneService.java | 9 ++ .../modules/done/model/DaysumBean.java | 48 +++++++-- .../modules/done/model/OvertimeBean.java | 62 +++++++++++ src/main/resources/static/css/style.css | 9 ++ src/main/resources/templates/done/list.html | 37 ++++++- 8 files changed, 264 insertions(+), 30 deletions(-) create mode 100644 src/main/java/de/jottyfan/timetrack/modules/done/model/OvertimeBean.java diff --git a/build.gradle b/build.gradle index d69fdb4..2118d5d 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/DoneController.java b/src/main/java/de/jottyfan/timetrack/modules/done/DoneController.java index 782ac7e..84b5a41 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneController.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneController.java @@ -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"; + } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/DoneRepository.java b/src/main/java/de/jottyfan/timetrack/modules/done/DoneRepository.java index 90a2f0e..4974d86 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneRepository.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneRepository.java @@ -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 OVERTIME = DSL.field("overtime", Integer.class); + SelectHavingStep> 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> 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 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 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 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(); } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/DoneService.java b/src/main/java/de/jottyfan/timetrack/modules/done/DoneService.java index 45f7d1c..795b6d4 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneService.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneService.java @@ -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()); + } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/model/DaysumBean.java b/src/main/java/de/jottyfan/timetrack/modules/done/model/DaysumBean.java index d27876b..88f22a0 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/model/DaysumBean.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/model/DaysumBean.java @@ -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; } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/model/OvertimeBean.java b/src/main/java/de/jottyfan/timetrack/modules/done/model/OvertimeBean.java new file mode 100644 index 0000000..03d0ab8 --- /dev/null +++ b/src/main/java/de/jottyfan/timetrack/modules/done/model/OvertimeBean.java @@ -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; + } +} diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 9568478..b914b1a 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -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; diff --git a/src/main/resources/templates/done/list.html b/src/main/resources/templates/done/list.html index 4310a24..68d800d 100644 --- a/src/main/resources/templates/done/list.html +++ b/src/main/resources/templates/done/list.html @@ -52,6 +52,8 @@ + +
@@ -104,8 +106,8 @@ Ende: Arbeitszeit total: Pausezeit total: - Überstunden heute: - Überstunden total: + Überstunden heute: + Überstunden total: @@ -213,6 +215,37 @@
+
+
+ +
+
+
+
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.
+
+
Tagesbeginn
+
+ +
+
Überstunden (min)
+
+ +
+
+
+ +
+
+
+
+
+
+
+ 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. +
+