diff --git a/build.gradle b/build.gradle index 6c6a5c2..9653496 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { apply plugin: 'io.spring.dependency-management' group = 'de.jottyfan' -version = '1.3.4' +version = '1.3.5' description = """timetrack""" @@ -46,6 +46,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-devtools' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' implementation 'de.jottyfan:timetrackjooq:0.1.2' 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 8cbcff0..7740b16 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneController.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.SessionAttributes; import de.jottyfan.timetrack.component.OAuth2Provider; import de.jottyfan.timetrack.modules.CommonController; @@ -25,20 +26,26 @@ import jakarta.annotation.security.RolesAllowed; * */ @Controller +@SessionAttributes("doneModel") public class DoneController extends CommonController { @Autowired private OAuth2Provider provider; - + @Autowired private ProfileService profileService; @Autowired private DoneService doneService; + @ModelAttribute("doneModel") + DoneModel getdoneModel() { + return new DoneModel(); + } + @RolesAllowed("timetrack_user") @RequestMapping(value = "/done/list") - public String getList(@ModelAttribute DoneModel doneModel, Model model) { + public String getList(Model model, @ModelAttribute("doneModel") DoneModel doneModel) { String username = provider.getName(); Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file LocalDate day = doneModel.getDay(); @@ -47,9 +54,9 @@ public class DoneController extends CommonController { SummaryBean bean = new SummaryBean(list, day, maxWorkTime); SummaryBean weekBean = new SummaryBean(week, day, maxWorkTime); model.addAttribute("doneList", list); - model.addAttribute("doneModel", doneModel); model.addAttribute("sum", bean); model.addAttribute("schedule", weekBean.toJson()); + model.addAttribute("recentList", doneService.getListRecent(username, 10)); model.addAttribute("projectList", doneService.getProjects(false)); model.addAttribute("moduleList", doneService.getModules(false)); model.addAttribute("jobList", doneService.getJobs(false)); @@ -61,9 +68,7 @@ public class DoneController extends CommonController { @RolesAllowed("timetrack_user") @GetMapping("/done/abort/{day}") public String abort(@PathVariable String day, Model model) { - DoneModel doneModel = new DoneModel(); - doneModel.setDayString(day); - return getList(doneModel, model); + return "redirect:/done/list"; } @RolesAllowed("timetrack_user") @@ -76,10 +81,7 @@ public class DoneController extends CommonController { private String toItem(DoneBean bean, Model model) { String username = provider.getName(); - DoneModel doneModel = new DoneModel(); - doneModel.setDay(bean.getLocalDate()); model.addAttribute("doneBean", bean); - model.addAttribute("doneModel", doneModel); model.addAttribute("projectList", doneService.getProjects(true)); model.addAttribute("moduleList", doneService.getModules(true)); model.addAttribute("jobList", doneService.getJobs(true)); @@ -98,23 +100,61 @@ public class DoneController extends CommonController { return toItem(bean, model); } + @RolesAllowed("timetrack_user") + @GetMapping("/done/end/{id}") + public String end(@PathVariable Integer id, Model model) { + DoneBean bean = doneService.getBean(id); + String username = provider.getName(); + doneService.endToNow(bean, username); + return "redirect:/done/list"; + } + + @RolesAllowed("timetrack_user") + @GetMapping("/done/copy/{id}") + public String copyFromNow(@PathVariable Integer id, Model model) { + DoneBean bean = doneService.getBean(id); + String username = provider.getName(); + doneService.copyFromNow(bean, username); + return "redirect:/done/list"; + } + @RolesAllowed("timetrack_user") @RequestMapping(value = "/done/upsert", method = RequestMethod.POST) public String doUpsert(Model model, @ModelAttribute DoneBean bean) { String username = provider.getName(); Integer amount = doneService.doUpsert(bean, username); - DoneModel doneModel = new DoneModel(); - doneModel.setDay(bean.getLocalDate()); - return amount.equals(1) ? getList(doneModel, model) : toItem(bean.getPk(), model); + return amount.equals(1) ? "redirect:/done/list" : "redirect:/" + toItem(bean.getPk(), model); + } + + @RolesAllowed("timetrack_user") + @RequestMapping(value = "/done/addrecent/{id}", method = RequestMethod.GET) + public String addRecent(Model model, @PathVariable Integer id) { + String username = provider.getName(); + DoneBean bean = doneService.getBean(id); + doneService.addRecent(bean, username); + return "redirect:/done/list"; + } + + @RolesAllowed("timetrack_user") + @RequestMapping(value = "/done/list/previousday", method = RequestMethod.GET) + public String previousDay(Model model, @ModelAttribute("doneModel") DoneModel doneModel) { + LocalDate day = doneModel.getDay(); + doneModel.setDay(day.minusDays(1)); + return "redirect:/done/list"; + } + + @RolesAllowed("timetrack_user") + @RequestMapping(value = "/done/list/nextday", method = RequestMethod.GET) + public String nextDay(Model model, @ModelAttribute("doneModel") DoneModel doneModel) { + LocalDate day = doneModel.getDay(); + doneModel.setDay(day.plusDays(1)); + return "redirect:/done/list"; } @RolesAllowed("timetrack_user") @GetMapping(value = "/done/delete/{id}") public String doDelete(@PathVariable Integer id, Model model) { - DoneBean bean = doneService.getBean(id); Integer amount = doneService.doDelete(id); - DoneModel doneModel = new DoneModel(); - doneModel.setDay(bean.getLocalDate()); - return amount.equals(1) ? getList(doneModel, model) : toItem(id, model); + return amount.equals(1) ? "redirect:/done/list" : "redirect:/" + toItem(id, model); } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/DoneGateway.java b/src/main/java/de/jottyfan/timetrack/modules/done/DoneGateway.java index 3e1aa76..c16ec6a 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneGateway.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneGateway.java @@ -1,7 +1,6 @@ package de.jottyfan.timetrack.modules.done; import static de.jottyfan.timetrack.db.done.Tables.T_DONE; -import static de.jottyfan.timetrack.db.done.Tables.V_BILLING; import static de.jottyfan.timetrack.db.done.Tables.V_JOB; import static de.jottyfan.timetrack.db.done.Tables.V_MODULE; import static de.jottyfan.timetrack.db.done.Tables.V_PROJECT; @@ -24,11 +23,13 @@ import org.jooq.InsertValuesStep7; import org.jooq.Record7; import org.jooq.Result; import org.jooq.SelectConditionStep; +import org.jooq.SelectLimitPercentStep; import org.jooq.UpdateConditionStep; import org.jooq.exception.DataAccessException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +import de.jottyfan.timetrack.db.done.tables.TDone; import de.jottyfan.timetrack.db.done.tables.records.TDoneRecord; import de.jottyfan.timetrack.db.done.tables.records.VBillingRecord; import de.jottyfan.timetrack.db.done.tables.records.VJobRecord; @@ -146,7 +147,7 @@ public class DoneGateway { if (includeNull) { list.add(new VBillingRecord()); } - list.addAll(getJooq().selectFrom(V_BILLING).orderBy(V_BILLING.NAME).fetchInto(VBillingRecord.class)); +// list.addAll(getJooq().selectFrom(V_BILLING).orderBy(V_BILLING.NAME).fetchInto(VBillingRecord.class)); return list; } @@ -266,6 +267,43 @@ public class DoneGateway { return list; } + public List getRecent(Integer userId, int recentCount) + throws DataAccessException, ClassNotFoundException, SQLException { + TDone X = T_DONE.as("x"); + + SelectLimitPercentStep> sql = jooq + // @formatter:off + .select(X.PK, X.TIME_FROM, X.TIME_UNTIL, X.FK_PROJECT, X.FK_MODULE, X.FK_JOB, X.FK_BILLING) + .from(jooq + .select(T_DONE.PK, T_DONE.TIME_FROM, T_DONE.TIME_UNTIL, T_DONE.FK_PROJECT, T_DONE.FK_MODULE, T_DONE.FK_JOB, T_DONE.FK_BILLING) + .distinctOn(T_DONE.FK_PROJECT, T_DONE.FK_MODULE, T_DONE.FK_JOB) + .from(T_DONE) + .where(T_DONE.FK_LOGIN.eq(userId)) + .asTable(X)) + .orderBy(X.TIME_FROM.desc()) + .limit(recentCount); + // @formatter:on + LOGGER.trace(sql); + List list = new ArrayList<>(); + Map projectMap = getProjectMap(); + Map moduleMap = getModuleMap(); + Map jobMap = getJobMap(); + Map billingMap = getBillingMap(); + for (Record7 r : sql.fetch()) { + DoneBean bean = new DoneBean(); + bean.setPk(r.get(T_DONE.PK)); + bean.setTimeFrom(r.get(T_DONE.TIME_FROM)); + bean.setTimeUntil(r.get(T_DONE.TIME_UNTIL)); + bean.setLocalDate(bean.getLocalDate()); + bean.setProject(projectMap.get(r.get(T_DONE.FK_PROJECT))); + bean.setModule(moduleMap.get(r.get(T_DONE.FK_MODULE))); + bean.setActivity(jobMap.get(r.get(T_DONE.FK_JOB))); + bean.setBilling(billingMap.get(r.get(T_DONE.FK_BILLING))); + list.add(bean); + } + return list; + } + /** * get list of entries of day * diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/DoneModel.java b/src/main/java/de/jottyfan/timetrack/modules/done/DoneModel.java index 5236585..a3ee4c2 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneModel.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneModel.java @@ -19,7 +19,7 @@ public class DoneModel implements Serializable { private LocalDate day; public DoneModel() { - this.day = LocalDate.now(); + day = LocalDate.now(); } public String getDayString() { 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 17aff1b..dc76034 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/DoneService.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/DoneService.java @@ -1,6 +1,7 @@ package de.jottyfan.timetrack.modules.done; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -29,6 +30,10 @@ import de.jottyfan.timetrack.modules.note.NoteService; @Transactional(transactionManager = "transactionManager") public class DoneService { private static final Logger LOGGER = LogManager.getLogger(NoteService.class); + private static final int INTERVAL = 15; + + @Autowired + private TimeService timeService; @Autowired private DSLContext dsl; @@ -142,4 +147,51 @@ public class DoneService { return -1; } } + + public Integer endToNow(DoneBean bean, String username) { + try { + DoneGateway gw = new DoneGateway(dsl); + Integer userId = gw.getUserId(username); + bean.setTimeUntil(timeService.roundTime(LocalDateTime.now(), INTERVAL)); + return gw.upsert(bean, userId); + } catch (Exception e) { + LOGGER.error(e); + return -1; + } + } + + public Integer copyFromNow(DoneBean bean, String username) { + try { + DoneGateway gw = new DoneGateway(dsl); + Integer userId = gw.getUserId(username); + bean.setTimeFrom(timeService.roundTime(LocalDateTime.now(), INTERVAL)); + bean.setTimeUntil(null); + bean.setPk(null); + return gw.upsert(bean, userId); + } catch (Exception e) { + LOGGER.error(e); + return -1; + } + } + + public List getListRecent(String username, int recentCount) { + try { + DoneGateway gw = new DoneGateway(dsl); + Integer userId = gw.getUserId(username); + if (userId == null) { + LOGGER.warn("userId of user {} is null", username); + } + return gw.getRecent( userId, recentCount); + } catch (Exception e) { + LOGGER.error(e); + return new ArrayList<>(); + } + } + + public Integer addRecent(DoneBean bean, String username) { + bean.setPk(null); + bean.setTimeFrom(timeService.roundTime(LocalDateTime.now(), INTERVAL)); + bean.setTimeUntil(null); + return this.doUpsert(bean, username); + } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/TimeService.java b/src/main/java/de/jottyfan/timetrack/modules/done/TimeService.java new file mode 100644 index 0000000..6982743 --- /dev/null +++ b/src/main/java/de/jottyfan/timetrack/modules/done/TimeService.java @@ -0,0 +1,36 @@ +package de.jottyfan.timetrack.modules.done; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; + +/** + * + * @author jotty + * + */ +@Service +public class TimeService { + + /** + * calculate the next time in interval + * @param givenTime the time given + * @param interval the interval to round up or down to + * @return the rounded time + */ + public LocalDateTime roundTime(LocalDateTime givenTime, int interval) { + if (givenTime == null) { + return null; + } else { + int minute = givenTime.getMinute(); + int compareMinute = minute % interval; + int offset = 0; + if (compareMinute <= (interval / 2)) { + offset = -compareMinute; + } else { + offset = interval - compareMinute; + } + return givenTime.plusMinutes(offset); + } + } +} diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/job/JobController.java b/src/main/java/de/jottyfan/timetrack/modules/done/job/JobController.java index 3e35523..7b5bfd7 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/job/JobController.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/job/JobController.java @@ -50,7 +50,7 @@ public class JobController extends CommonController { @RequestMapping(value = "/done/upsert/job", method = RequestMethod.POST) public String doUpsert(Model model, @ModelAttribute TJobRecord bean) { Integer amount = jobService.doUpsert(bean); - return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toJob(bean.getPk(), model); + return amount.equals(1) ? "redirect:/done/list": toJob(bean.getPk(), model); } @RolesAllowed("timetrack_user") @@ -63,6 +63,6 @@ public class JobController extends CommonController { @GetMapping(value = "/done/delete/job/{id}") public String doDeleteJob(@PathVariable Integer id, Model model) { Integer amount = jobService.doDelete(id); - return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toJob(id, model); + return amount.equals(1) ? "redirect:/done/list" : toJob(id, model); } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/module/ModuleController.java b/src/main/java/de/jottyfan/timetrack/modules/done/module/ModuleController.java index fad9fcf..b462d45 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/module/ModuleController.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/module/ModuleController.java @@ -51,7 +51,7 @@ public class ModuleController extends CommonController { @RequestMapping(value = "/done/upsert/module", method = RequestMethod.POST) public String doUpsert(Model model, @ModelAttribute TModuleRecord bean) { Integer amount = moduleService.doUpsert(bean); - return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toModule(bean.getPk(), model); + return amount.equals(1) ? "redirect:/done/list" : toModule(bean.getPk(), model); } @RolesAllowed("timetrack_user") @@ -64,6 +64,6 @@ public class ModuleController extends CommonController { @GetMapping(value = "/done/delete/module/{id}") public String doDeleteModule(@PathVariable Integer id, Model model) { Integer amount = moduleService.doDelete(id); - return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toModule(id, model); + return amount.equals(1) ? "redirect:/done/list" : toModule(id, model); } } diff --git a/src/main/java/de/jottyfan/timetrack/modules/done/project/ProjectController.java b/src/main/java/de/jottyfan/timetrack/modules/done/project/ProjectController.java index 942abf3..48cb6d5 100644 --- a/src/main/java/de/jottyfan/timetrack/modules/done/project/ProjectController.java +++ b/src/main/java/de/jottyfan/timetrack/modules/done/project/ProjectController.java @@ -50,7 +50,7 @@ public class ProjectController extends CommonController { @RequestMapping(value = "/done/upsert/project", method = RequestMethod.POST) public String doUpsert(Model model, @ModelAttribute TProjectRecord bean) { Integer amount = projectService.doUpsert(bean); - return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toProject(bean.getPk(), model); + return amount.equals(1) ? "redirect:/done/list" : toProject(bean.getPk(), model); } @RolesAllowed("timetrack_user") @@ -63,6 +63,6 @@ public class ProjectController extends CommonController { @GetMapping(value = "/done/delete/project/{id}") public String doDeleteProject(@PathVariable Integer id, Model model) { Integer amount = projectService.doDelete(id); - return amount.equals(1) ? doneController.getList(new DoneModel(), model) : toProject(id, model); + return amount.equals(1) ? "redirect:/done/list" : toProject(id, model); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bded151..79aacb7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -20,5 +20,5 @@ spring.security.oauth2.client.provider.keycloak.jwk-set-uri = ${keycloak.openid- spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username # application -server.port = 9001 +server.port = ${server.port} server.servlet.context-path = /timetrack diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index e3d1fe3..66d7ff4 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -42,6 +42,13 @@ body { background-color: #222; } +@media(min-width:1600px) { + .tabdivblurred { + width: 50%; + margin: auto; + } +} + .tabdivblurred { padding: 8px; padding-bottom: 0px; diff --git a/src/main/resources/templates/done/list.html b/src/main/resources/templates/done/list.html index 08197b8..409b805 100644 --- a/src/main/resources/templates/done/list.html +++ b/src/main/resources/templates/done/list.html @@ -8,7 +8,8 @@ Arbeitszeit
    @@ -24,6 +26,19 @@ Neuer Eintrag + + + @@ -54,7 +69,9 @@ - + + + @@ -64,7 +81,9 @@ th:text="${done.activity?.name}"> - + + + diff --git a/src/test/java/de/jottyfan/timetrack/modules/done/TestTimeService.java b/src/test/java/de/jottyfan/timetrack/modules/done/TestTimeService.java new file mode 100644 index 0000000..c17055e --- /dev/null +++ b/src/test/java/de/jottyfan/timetrack/modules/done/TestTimeService.java @@ -0,0 +1,41 @@ +package de.jottyfan.timetrack.modules.done; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import org.junit.jupiter.api.Test; + +/** + * + * @author jotty + * + */ +public class TestTimeService { + + @Test + public void testRoundTime() { + TimeService service = new TimeService(); + LocalDateTime today = LocalDateTime.now(); + assertEquals("01:00", service.roundTime(today.withHour(1).withMinute(7), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(8), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(9), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(10), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(11), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(12), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(13), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(14), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(15), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(16), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(17), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(18), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(19), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(20), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(21), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:15", service.roundTime(today.withHour(1).withMinute(22), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:30", service.roundTime(today.withHour(1).withMinute(23), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("01:45", service.roundTime(today.withHour(1).withMinute(52), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + assertEquals("02:00", service.roundTime(today.withHour(1).withMinute(53), 15).format(DateTimeFormatter.ofPattern("HH:mm"))); + } +}