Compare commits

...

32 Commits

Author SHA1 Message Date
d95b3a1600 library upgrades 2025-03-03 22:54:44 +01:00
2ef8a48488 finetuning 2025-01-09 19:04:31 +01:00
5dcc64ac74 support lange desktops 2025-01-09 13:41:16 +01:00
23bab9a2b4 added new billing ElementSearcher style 2025-01-06 14:37:30 +01:00
1d532e322c editing inline in table by direct request 2024-12-02 17:40:46 +01:00
c757bb5916 library upgrades 2024-06-11 21:25:48 +02:00
c4615765a5 another finetuning 2024-01-10 09:49:01 +01:00
5b296d39e9 finetuning 2024-01-10 09:40:04 +01:00
a4bcc00363 display finetuning 2024-01-09 17:04:48 +01:00
4820232b31 summary time calculation enhanced 2024-01-09 16:44:05 +01:00
f8f501f1b2 added dynamic css 2024-01-09 10:17:00 +01:00
a52793de46 corrected date change 2024-01-05 21:26:53 +01:00
e38f62fa72 fixed overtime calculation 2024-01-05 21:18:32 +01:00
1f71d9edeb added bulk slot creation 2024-01-05 17:30:04 +01:00
b779590309 total overtime correction 2024-01-05 16:01:46 +01:00
689a601c8c optimized reason for slot difference 2024-01-05 15:12:32 +01:00
5117fd0e71 preparation for slot dimediff reason 2024-01-05 14:58:40 +01:00
c1b8283dd0 code cleanup 2024-01-05 11:09:53 +01:00
7fc30ffe48 calendar like slot overview 2024-01-05 10:42:05 +01:00
568dfc8a64 slots basic info 2024-01-04 21:53:13 +01:00
8be05b8afc overtime calculation optimized 2024-01-04 20:35:26 +01:00
742446e46e corrected total overtime calculation 2024-01-04 10:01:58 +01:00
48168aaf65 basic overtime calculation corrections; needs slots 2024-01-03 17:49:26 +01:00
9373eacab7 use start time from now rounded for favorites, too 2023-11-08 17:06:24 +01:00
8b51b595d6 added favorite usage 2023-11-02 18:57:09 +01:00
d702d6816b prepared favorites 2023-11-02 18:07:34 +01:00
f11723505e finetuning 2023-11-01 23:23:09 +01:00
a737adf8c1 set seconds and milliseconds to zero 2023-10-18 16:24:23 +02:00
4f5db460ae eye candy 2023-10-17 14:19:59 +02:00
ee41117a57 reintegrated summary of billings 2023-10-17 11:14:50 +02:00
e7d9d74269 improvements from Maik 2023-10-16 22:36:41 +02:00
0cc5cdb945 switch theme on every page 2023-09-29 17:29:08 +02:00
42 changed files with 2243 additions and 321 deletions

View File

@ -25,6 +25,11 @@
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>

View File

@ -0,0 +1,2 @@
boot.validation.initialized=true
eclipse.preferences.version=1

View File

@ -1,5 +1,5 @@
plugins {
id 'org.springframework.boot' version '3.1.3'
id 'org.springframework.boot' version '3.4.3'
id 'java'
id 'war'
}
@ -7,7 +7,7 @@ plugins {
apply plugin: 'io.spring.dependency-management'
group = 'de.jottyfan'
version = '1.3.3'
version = '1.5.7'
description = """timetrack"""
@ -23,33 +23,33 @@ repositories {
}
dependencies {
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.20.0'
implementation 'de.jottyfan:timetrackjooq:20240109'
implementation 'org.webjars:bootstrap:5.3.1'
implementation 'org.webjars:font-awesome:6.4.2'
implementation 'org.apache.logging.log4j:log4j-api:2.24.3'
implementation 'org.apache.logging.log4j:log4j-core:2.24.3'
implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.24.3'
implementation 'org.webjars:bootstrap:5.3.3'
implementation 'org.webjars:font-awesome:6.7.2'
implementation 'org.webjars:jquery:3.7.1'
implementation 'org.webjars:popper.js:2.11.7'
implementation 'org.webjars:datatables:1.13.5'
implementation 'org.webjars:jquery-ui:1.13.2'
implementation 'org.webjars:fullcalendar:5.11.3'
implementation 'org.webjars:datatables:2.1.8'
implementation 'org.webjars:jquery-ui:1.14.1'
implementation 'org.webjars:fullcalendar:6.1.9'
implementation 'com.google.code.gson:gson:2.10.1';
implementation 'org.webjars.bowergithub.datatables:datatables:1.10.21'
implementation 'com.google.code.gson:gson:latest.release';
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:1.1.2'
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:1.4.2'
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'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.2.1'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:latest.release'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
@ -65,9 +65,9 @@ war {
"Implementation-Timestamp": new Date())
}
}
baseName = project.name
version = version
archiveName = 'timetrack.war'
archiveBaseName = project.name
archiveVersion = version
archiveFileName = 'timetrack.war'
}
test {

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,5 +1,7 @@
package de.jottyfan.timetrack;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@ -8,15 +10,17 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
public class TimetrackApplication extends SpringBootServletInitializer {
public class Main extends SpringBootServletInitializer {
public static final Logger LOGGER = LogManager.getLogger(Main.class);
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder application) {
return application.sources(TimetrackApplication.class);
return application.sources(Main.class);
}
public static void main(String[] args) {
SpringApplication.run(TimetrackApplication.class, args);
SpringApplication.run(Main.class, args);
}
}

View File

@ -11,7 +11,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.done.model.DoneModel;
/**
*
@ -39,8 +39,9 @@ public class InitialConfiguration {
}
@Bean
public void disableLogo() {
public Boolean disableLogo() {
System.setProperty("org.jooq.no-logo", "true");
return true;
}
public DefaultConfiguration configuration() {

View File

@ -0,0 +1,35 @@
package de.jottyfan.timetrack.modules;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ModelAttribute;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.profile.ProfileService;
/**
*
* @author jotty
*
*/
public abstract class CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@Value("${server.servlet.context-path}")
private String contextPath;
@ModelAttribute("baseUrl")
public String getBaseUrl() {
return contextPath;
}
@ModelAttribute("theme")
public String getTheme() {
String username = provider.getName();
return profileService.getTheme(username);
}
}

View File

@ -12,14 +12,12 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.done.DoneBean;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.done.DoneService;
import de.jottyfan.timetrack.modules.done.SummaryBean;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import de.jottyfan.timetrack.modules.done.model.DoneBean;
import de.jottyfan.timetrack.modules.done.model.DoneModel;
import de.jottyfan.timetrack.modules.done.model.SummaryBean;
import jakarta.annotation.security.RolesAllowed;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -30,7 +28,7 @@ import jakarta.servlet.http.HttpServletRequest;
*
*/
@Controller
public class IndexController {
public class IndexController extends CommonController {
private static final Logger LOGGER = LogManager.getLogger(IndexController.class);
@Autowired
@ -39,9 +37,6 @@ public class IndexController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@GetMapping("/logout")
public String getLogout(HttpServletRequest request) throws ServletException {
request.logout();
@ -49,14 +44,13 @@ public class IndexController {
}
@RolesAllowed("timetrack_user")
@RequestMapping("/")
public String getIndex(@ModelAttribute DoneModel doneModel, Model model, OAuth2AuthenticationToken token) {
@GetMapping("/")
public String getIndex(@ModelAttribute("doneModel") DoneModel doneModel, Model model, OAuth2AuthenticationToken token) {
String username = provider.getName();
Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file
LocalDate day = LocalDate.now();
List<DoneBean> list = doneService.getList(day, username);
model.addAttribute("sum", new SummaryBean(list, day, maxWorkTime));
model.addAttribute("theme", profileService.getTheme(username));
LOGGER.debug("sum = {}", model.getAttribute("sum"));
return "public/index";
}

View File

@ -5,8 +5,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import de.jottyfan.timetrack.modules.CommonController;
/**
*
@ -14,13 +13,7 @@ import de.jottyfan.timetrack.modules.profile.ProfileService;
*
*/
@Controller
public class CalendarController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
public class CalendarController extends CommonController {
@Autowired
private CalendarService service;
@ -28,7 +21,6 @@ public class CalendarController {
@GetMapping("/calendar")
public String getCalendar(Model model) {
model.addAttribute("events", service.getJsonEvents());
model.addAttribute("theme", profileService.getTheme(provider.getName()));
return "/calendar/calendar";
}
}

View File

@ -61,14 +61,13 @@ public class CalendarDoneRepository {
String billing = r.get(T_BILLING.NAME);
LocalDateTime start = r.get(T_DONE.TIME_FROM);
LocalDateTime end = r.get(T_DONE.TIME_UNTIL);
StringBuilder buf = new StringBuilder();
buf.append(billing).append(billing == null ? "" : "; ");
buf.append(job).append(job == null ? "" : " - ");
buf.append(module).append(module == null ? "" : ": ");
buf.append(project);
String title = buf.toString();
String title = String.format("%s %s %s %s", blankIfNull(billing, "; "), blankIfNull(project, " - "), blankIfNull(module, ": "), blankIfNull(job, "")).trim();
list.add(EventBean.ofEvent(id, title, start, end));
}
return list;
}
private final String blankIfNull(String s, String appendix) {
return s == null ? "" : s.concat(appendix);
}
}

View File

@ -10,13 +10,11 @@ import org.springframework.ui.Model;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.contact.enums.EnumContacttype;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import de.jottyfan.timetrack.modules.CommonController;
import jakarta.annotation.security.RolesAllowed;
/**
@ -25,13 +23,7 @@ import jakarta.annotation.security.RolesAllowed;
*
*/
@Controller
public class ContactController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
public class ContactController extends CommonController {
@Autowired
private ContactService contactService;
@ -43,43 +35,41 @@ public class ContactController {
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/contact/list")
@GetMapping("/contact/list")
public String getList(Model model) {
List<ContactBean> list = contactService.getList();
model.addAttribute("contactList", list);
model.addAttribute("theme", profileService.getTheme(provider.getName()));
return "contact/list";
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/contact/add", method = RequestMethod.GET)
@GetMapping("/contact/add")
public String toAdd(Model model) {
return toItem(null, model);
}
@RolesAllowed("timetrack_user")
@GetMapping("/contact/edit/{id}")
public String toItem(@PathVariable Integer id, Model model) {
public String toItem(@PathVariable("id") Integer id, Model model) {
ContactBean bean = contactService.getBean(id);
if (bean == null) {
bean = new ContactBean(); // the add case
}
model.addAttribute("contactBean", bean);
model.addAttribute("types", Arrays.asList(EnumContacttype.values()));
model.addAttribute("theme", profileService.getTheme(provider.getName()));
return "contact/item";
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/contact/upsert", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute ContactBean bean) {
@PostMapping("/contact/upsert")
public String doUpsert(Model model, @ModelAttribute("bean") ContactBean bean) {
Integer amount = contactService.doUpsert(bean);
return amount.equals(1) ? getList(model) : toItem(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/contact/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model) {
public String doDelete(@PathVariable("id") Integer id, Model model) {
Integer amount = contactService.doDelete(id);
return amount.equals(1) ? getList(model) : toItem(id, model);
}

View File

@ -11,12 +11,20 @@ import org.springframework.ui.Model;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.profile.ProfileService;
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.SlotBean;
import de.jottyfan.timetrack.modules.done.model.SlotRangeBean;
import de.jottyfan.timetrack.modules.done.model.SummaryBean;
import jakarta.annotation.security.RolesAllowed;
import jakarta.websocket.server.PathParam;
/**
*
@ -24,72 +32,90 @@ import jakarta.annotation.security.RolesAllowed;
*
*/
@Controller
public class DoneController {
@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) {
@GetMapping("/done/list")
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();
List<DoneBean> list = doneService.getList(day, username);
List<DoneBean> week = doneService.getWeek(day, username);
SummaryBean bean = new SummaryBean(list, day, maxWorkTime);
SummaryBean sumBean = new SummaryBean(list, day, maxWorkTime);
SummaryBean weekBean = new SummaryBean(week, day, maxWorkTime);
model.addAttribute("doneList", list);
model.addAttribute("doneModel", doneModel);
model.addAttribute("sum", bean);
Duration sumtimeDuration = Duration.ofMinutes(0);
for (DoneBean bean : list) {
sumtimeDuration = sumtimeDuration.plus(bean.getTimeDiffDuration());
}
model.addAttribute("sumtime", String.format("%02d:%02d", sumtimeDuration.toHours(), sumtimeDuration.toMinutes() % 60));
model.addAttribute("sum", sumBean);
model.addAttribute("daysum", doneService.getDaysum(day, username));
model.addAttribute("overtimeBean", doneService.getOvertimeBean(username));
model.addAttribute("slots", doneService.getSlots(day, username));
model.addAttribute("slotOffset", doneService.getSlotOffset(day));
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));
model.addAttribute("billingList", doneService.getBillings(false));
model.addAttribute("theme", profileService.getTheme(username));
model.addAttribute("favorites", doneService.getFavorites(username));
return "done/list";
}
@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);
@PostMapping("/done/list")
public String getListForDate(Model model, @ModelAttribute("doneModel") DoneModel doneModel) {
return getList(model, doneModel);
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/add/{day}", method = RequestMethod.GET)
public String toAdd(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate day, Model model) {
@GetMapping("/done/update/{id}")
public String updateField(@PathVariable("id") Integer fkDone, @PathParam("field") String field, @PathParam("value") Integer value) {
doneService.updateField(fkDone, field, value);
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/abort/{day}")
public String abort(@PathVariable("day") String day, Model model) {
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/add/{day}")
public String toAdd(@PathVariable("day") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate day, Model model) {
DoneBean bean = new DoneBean();
bean.setLocalDate(day);
return toItem(bean, model);
}
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));
model.addAttribute("billingList", doneService.getBillings(true));
model.addAttribute("theme", profileService.getTheme(username));
return "done/item";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/edit/{id}")
public String toItem(@PathVariable Integer id, Model model) {
public String toItem(@PathVariable("id") Integer id, Model model) {
DoneBean bean = doneService.getBean(id);
if (bean == null) {
bean = new DoneBean(); // the add case; typically, only add from today
@ -98,22 +124,149 @@ public class DoneController {
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute DoneBean bean) {
@GetMapping("/done/end/{id}")
public String end(@PathVariable("id") 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("id") Integer id, Model model) {
DoneBean bean = doneService.getBean(id);
String username = provider.getName();
doneService.copyFromNow(bean, username);
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@PostMapping("/done/upsert")
public String doUpsert(Model model, @ModelAttribute("bean") 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")
@GetMapping("/done/addrecent/{id}")
public String addRecent(Model model, @PathVariable("id") Integer id) {
String username = provider.getName();
DoneBean bean = doneService.getBean(id);
doneService.addRecent(bean, username);
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/list/previousday")
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")
@GetMapping("/done/list/nextday")
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);
public String doDelete(@PathVariable("id") Integer id, Model model) {
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);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/favorize/{id}")
public String favorize(@PathVariable("id") Integer id) {
doneService.favorize(id);
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/unfavorize/{id}")
public String unfavorize(@PathVariable("id") Integer id) {
doneService.unfavorize(id);
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/usefav/{id}")
public String usefavorite(@PathVariable("id") Integer id) {
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";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/slot/{id}")
public String loadSlot(@PathVariable("id") Integer id, Model model) {
String username = provider.getName();
model.addAttribute("bean", doneService.getSlot(id, username));
return "/done/slot/item";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/slot/add")
public String addSlot(@RequestParam("day") LocalDate day, Model model) {
model.addAttribute("bean", SlotBean.of(day));
return "/done/slot/item";
}
@RolesAllowed("timetrack_user")
@PostMapping("/done/slot/upsert")
public String upsertSlot(@ModelAttribute("bean") SlotBean bean, Model model) {
doneService.upsert(bean, provider.getName());
return "redirect:/done/list#div_slot";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/slot/{id}/delete")
public String deleteSlot(@PathVariable("id") Integer slotId) {
doneService.delete(slotId, provider.getName());
return "redirect:/done/list#div_slot";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/slot/range")
public String toAddRange(Model model) {
model.addAttribute("bean", new SlotRangeBean());
return "/done/slot/range";
}
@RolesAllowed("timetrack_user")
@PostMapping("/done/slot/addrange")
public String addRange(@ModelAttribute("bean") SlotRangeBean bean) {
doneService.addSlotRange(bean.getMinutes(), bean.getFrom(), bean.getUntil(), bean.getReason(), provider.getName(),
bean.getIncludeSaturday(), bean.getIncludeSunday());
return "redirect:/done/list#div_slot";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/slot/back")
public String oneMonthBack(Model model, @ModelAttribute("doneModel") DoneModel doneModel) {
LocalDate day = doneModel.getDay();
doneModel.setDay(day.minusMonths(1));
return "redirect:/done/list#div_slot";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/slot/forward")
public String oneMonthForward(Model model, @ModelAttribute("doneModel") DoneModel doneModel) {
LocalDate day = doneModel.getDay();
doneModel.setDay(day.plusMonths(1));
return "redirect:/done/list#div_slot";
}
}

View File

@ -1,6 +1,7 @@
package de.jottyfan.timetrack.modules.done;
import static de.jottyfan.timetrack.db.done.Tables.T_DONE;
import static de.jottyfan.timetrack.db.done.Tables.T_FAVORITE;
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;
@ -20,22 +21,32 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertOnDuplicateStep;
import org.jooq.InsertReturningStep;
import org.jooq.InsertValuesStep7;
import org.jooq.Record5;
import org.jooq.Record7;
import org.jooq.Record8;
import org.jooq.Result;
import org.jooq.SelectConditionStep;
import org.jooq.SelectLimitPercentStep;
import org.jooq.UpdateConditionStep;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
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.TFavoriteRecord;
import de.jottyfan.timetrack.db.done.tables.records.VBillingRecord;
import de.jottyfan.timetrack.db.done.tables.records.VJobRecord;
import de.jottyfan.timetrack.db.done.tables.records.VModuleRecord;
import de.jottyfan.timetrack.db.done.tables.records.VProjectRecord;
import de.jottyfan.timetrack.db.profile.tables.records.TLoginRecord;
import de.jottyfan.timetrack.help.LocalDateHelper;
import de.jottyfan.timetrack.modules.done.model.DoneBean;
import de.jottyfan.timetrack.modules.done.model.FavoriteBean;
/**
*
@ -230,7 +241,7 @@ public class DoneGateway {
*/
private List<DoneBean> getAllOfInterval(LocalDateTime start, LocalDateTime end, Integer userId)
throws DataAccessException, ClassNotFoundException, SQLException {
SelectConditionStep<Record7<Integer, LocalDateTime, LocalDateTime, Integer, Integer, Integer, Integer>> sql = getJooq()
SelectConditionStep<Record8<Integer, LocalDateTime, LocalDateTime, Integer, Integer, Integer, Integer, Integer>> sql = getJooq()
// @formatter:off
.select(T_DONE.PK,
T_DONE.TIME_FROM,
@ -238,8 +249,14 @@ public class DoneGateway {
T_DONE.FK_PROJECT,
T_DONE.FK_MODULE,
T_DONE.FK_JOB,
T_DONE.FK_BILLING)
T_DONE.FK_BILLING,
T_FAVORITE.PK_FAVORITE)
.from(T_DONE)
.leftJoin(T_FAVORITE).on(T_FAVORITE.FK_LOGIN.eq(T_DONE.FK_LOGIN))
.and(T_FAVORITE.FK_PROJECT.eq(T_DONE.FK_PROJECT).or(T_FAVORITE.FK_PROJECT.isNull().and(T_DONE.FK_PROJECT.isNull())))
.and(T_FAVORITE.FK_MODULE.eq(T_DONE.FK_MODULE).or(T_FAVORITE.FK_MODULE.isNull().and(T_DONE.FK_MODULE.isNull())))
.and(T_FAVORITE.FK_JOB.eq(T_DONE.FK_JOB).or(T_FAVORITE.FK_JOB.isNull().and(T_DONE.FK_JOB.isNull())))
.and(T_FAVORITE.FK_BILLING.eq(T_DONE.FK_BILLING).or(T_FAVORITE.FK_BILLING.isNull().and(T_DONE.FK_BILLING.isNull())))
.where(T_DONE.TIME_FROM.between(start, end).or(T_DONE.TIME_FROM.isNull()))
.and(T_DONE.TIME_UNTIL.between(start, end).or(T_DONE.TIME_UNTIL.isNull()))
.and(T_DONE.FK_LOGIN.eq(userId == null ? -999999 : userId));
@ -250,6 +267,45 @@ public class DoneGateway {
Map<Integer, VModuleRecord> moduleMap = getModuleMap();
Map<Integer, VJobRecord> jobMap = getJobMap();
Map<Integer, VBillingRecord> billingMap = getBillingMap();
for (Record8<Integer, LocalDateTime, LocalDateTime, Integer, Integer, Integer, Integer, Integer> 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)));
bean.setIsFavorite(r.get(T_FAVORITE.PK_FAVORITE) != null);
list.add(bean);
}
list.sort((o1, o2) -> o1 == null ? 0 : o1.compareTo(o2));
return list;
}
public List<DoneBean> getRecent(Integer userId, int recentCount)
throws DataAccessException, ClassNotFoundException, SQLException {
TDone X = T_DONE.as("x");
SelectLimitPercentStep<Record7<Integer, LocalDateTime, LocalDateTime, Integer, Integer, Integer, Integer>> 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<DoneBean> list = new ArrayList<>();
Map<Integer, VProjectRecord> projectMap = getProjectMap();
Map<Integer, VModuleRecord> moduleMap = getModuleMap();
Map<Integer, VJobRecord> jobMap = getJobMap();
Map<Integer, VBillingRecord> billingMap = getBillingMap();
for (Record7<Integer, LocalDateTime, LocalDateTime, Integer, Integer, Integer, Integer> r : sql.fetch()) {
DoneBean bean = new DoneBean();
bean.setPk(r.get(T_DONE.PK));
@ -262,7 +318,6 @@ public class DoneGateway {
bean.setBilling(billingMap.get(r.get(T_DONE.FK_BILLING)));
list.add(bean);
}
list.sort((o1, o2) -> o1 == null ? 0 : o1.compareTo(o2));
return list;
}
@ -397,4 +452,90 @@ public class DoneGateway {
LOGGER.debug(sql.toString());
return sql.execute();
}
public void favorize(Integer id) {
InsertReturningStep<TFavoriteRecord> sql = getJooq()
// @formatter:off
.insertInto(T_FAVORITE,
T_FAVORITE.FK_LOGIN,
T_FAVORITE.FK_PROJECT,
T_FAVORITE.FK_MODULE,
T_FAVORITE.FK_JOB,
T_FAVORITE.FK_BILLING)
.select(getJooq()
.select(T_DONE.FK_LOGIN, T_DONE.FK_PROJECT, T_DONE.FK_MODULE, T_DONE.FK_JOB, T_DONE.FK_BILLING)
.from(T_DONE)
.where(T_DONE.PK.eq(id)))
// TODO: create unique constraint
/*
.onConflict(T_FAVORITE.FK_LOGIN, T_FAVORITE.FK_PROJECT, T_FAVORITE.FK_MODULE, T_FAVORITE.FK_JOB, T_FAVORITE.FK_BILLING)
.doNothing()*/
;
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
public void unfavorize(Integer id) {
DeleteConditionStep<TFavoriteRecord> sql = getJooq()
// @formatter:off
.deleteFrom(T_FAVORITE)
.using(T_DONE)
.where(T_FAVORITE.FK_LOGIN.eq(T_DONE.FK_LOGIN))
.and(T_FAVORITE.FK_PROJECT.eq(T_DONE.FK_PROJECT).or(T_FAVORITE.FK_PROJECT.isNull().and(T_DONE.FK_PROJECT.isNull())))
.and(T_FAVORITE.FK_MODULE.eq(T_DONE.FK_MODULE).or(T_FAVORITE.FK_MODULE.isNull().and(T_DONE.FK_MODULE.isNull())))
.and(T_FAVORITE.FK_JOB.eq(T_DONE.FK_JOB).or(T_FAVORITE.FK_JOB.isNull().and(T_DONE.FK_JOB.isNull())))
.and(T_FAVORITE.FK_BILLING.eq(T_DONE.FK_BILLING).or(T_FAVORITE.FK_BILLING.isNull().and(T_DONE.FK_BILLING.isNull())))
.and(T_DONE.PK.eq(id));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
public List<FavoriteBean> getFavorites(Integer login) {
SelectConditionStep<Record5<Integer, String, String, String, String>> sql = getJooq()
// @formatter:off
.select(T_FAVORITE.PK_FAVORITE,
V_PROJECT.NAME,
V_MODULE.NAME,
V_JOB.NAME,
V_BILLING.NAME)
.from(T_FAVORITE)
.leftJoin(V_PROJECT).on(V_PROJECT.PK.eq(T_FAVORITE.FK_PROJECT))
.leftJoin(V_MODULE).on(V_MODULE.PK.eq(T_FAVORITE.FK_MODULE))
.leftJoin(V_JOB).on(V_JOB.PK.eq(T_FAVORITE.FK_JOB))
.leftJoin(V_BILLING).on(V_BILLING.PK.eq(T_FAVORITE.FK_BILLING))
.where(T_FAVORITE.FK_LOGIN.eq(login));
// @formatter:on
LOGGER.trace(sql);
List<FavoriteBean> list = new ArrayList<>();
for (Record5<Integer, String, String, String, String> r : sql.fetch()) {
FavoriteBean bean = new FavoriteBean();
bean.setFkFavorite(r.get(T_FAVORITE.PK_FAVORITE));
bean.setProject(r.get(V_PROJECT.NAME));
bean.setModule(r.get(V_MODULE.NAME));
bean.setJob(r.get(V_JOB.NAME));
bean.setBilling(r.get(V_BILLING.NAME));
list.add(bean);
}
return list;
}
/**
* add a new entry as the favorite tells
*
* @param fkFavorite the id of the favorite
*/
public void useFav(Integer fkFavorite, LocalDateTime startTime) {
InsertOnDuplicateStep<TDoneRecord> sql = getJooq()
// @formatter:off
.insertInto(T_DONE, T_DONE.FK_LOGIN, T_DONE.FK_PROJECT, T_DONE.FK_MODULE, T_DONE.FK_JOB, T_DONE.FK_BILLING, T_DONE.TIME_FROM)
.select(getJooq()
.select(T_FAVORITE.FK_LOGIN, T_FAVORITE.FK_PROJECT, T_FAVORITE.FK_MODULE, T_FAVORITE.FK_JOB, T_FAVORITE.FK_BILLING, DSL.val(startTime))
.from(T_FAVORITE)
.where(T_FAVORITE.PK_FAVORITE.eq(fkFavorite)));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
}

View File

@ -0,0 +1,319 @@
package de.jottyfan.timetrack.modules.done;
import static de.jottyfan.timetrack.db.done.Tables.T_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.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.DatePart;
import org.jooq.DeleteConditionStep;
import org.jooq.Field;
import org.jooq.InsertOnDuplicateSetMoreStep;
import org.jooq.InsertOnDuplicateStep;
import org.jooq.InsertReturningStep;
import org.jooq.Record1;
import org.jooq.Record3;
import org.jooq.Record4;
import org.jooq.Record5;
import org.jooq.Row4;
import org.jooq.SelectConditionStep;
import org.jooq.SelectHavingStep;
import org.jooq.SelectSeekStep1;
import org.jooq.TableField;
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.TDoneRecord;
import de.jottyfan.timetrack.db.done.tables.records.TOvertimeRecord;
import de.jottyfan.timetrack.db.done.tables.records.TRequiredWorktimeRecord;
import de.jottyfan.timetrack.modules.done.model.DaysumBean;
import de.jottyfan.timetrack.modules.done.model.OvertimeBean;
import de.jottyfan.timetrack.modules.done.model.SlotBean;
/**
*
* @author jotty
*
*/
@Repository
public class DoneRepository {
private static final Logger LOGGER = LogManager.getLogger(DoneRepository.class);
@Autowired
private DSLContext jooq;
public DaysumBean getDaysum(LocalDate day, String login) {
Field<LocalTime> WORKTIME = DSL.field("worktime", LocalTime.class);
Field<LocalTime> BREAKTIME = DSL.field("breaktime", LocalTime.class);
SelectConditionStep<Record5<LocalTime, LocalTime, LocalTime, LocalTime, YearToSecond>> sql = jooq
// @formatter:off
.select(V_DAY.STARTTIME,
V_DAY.ENDTIME,
V_DAY.WORKTIME.cast(LocalTime.class).as(WORKTIME),
V_DAY.BREAKTIME.cast(LocalTime.class).as(BREAKTIME),
V_DAY.DAY_OVERTIME)
.from(V_DAY)
.innerJoin(T_LOGIN).on(T_LOGIN.PK.eq(V_DAY.FK_LOGIN))
.where(V_DAY.DAY.eq(day))
.and(T_LOGIN.LOGIN.eq(login));
// @formatter:on
LOGGER.trace(sql);
Record5<LocalTime, LocalTime, LocalTime, LocalTime, YearToSecond> r = sql.fetchOne();
if (r == null) {
return null;
} else {
DaysumBean bean = new DaysumBean();
bean.setDaytimeFrom(r.get(V_DAY.STARTTIME));
bean.setDaytimeUntil(r.get(V_DAY.ENDTIME));
bean.setDayworktime(r.get(WORKTIME));
bean.setBreaks(r.get(BREAKTIME));
YearToSecond dayOvertime = r.get(V_DAY.DAY_OVERTIME);
Duration dayOvertimeDuration = dayOvertime == null ? null : dayOvertime.toDuration();
bean.setDayOvertime(
dayOvertimeDuration == null ? null : Long.valueOf(dayOvertimeDuration.toMinutes()).intValue());
bean.setTotalOvertime(getOvertimeOf(day, login));
return bean;
}
}
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.EPOCH).div(60).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.le(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);
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();
}
public Map<LocalDate, SlotBean> getSlots(LocalDate from, LocalDate until, String login) {
SelectSeekStep1<Record4<Integer, LocalDate, String, Integer>, LocalDate> sql = jooq
// @formatter:off
.select(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME,
T_REQUIRED_WORKTIME.DAY,
T_REQUIRED_WORKTIME.REASON,
T_REQUIRED_WORKTIME.REQUIRED_MINUTES)
.from(T_REQUIRED_WORKTIME)
.innerJoin(T_LOGIN).on(T_LOGIN.PK.eq(T_REQUIRED_WORKTIME.FK_LOGIN))
.where(T_LOGIN.LOGIN.eq(login))
.and(T_REQUIRED_WORKTIME.DAY.ge(from))
.and(T_REQUIRED_WORKTIME.DAY.le(until))
.orderBy(T_REQUIRED_WORKTIME.DAY);
// @formatter:on
LOGGER.trace(sql);
Iterator<Record4<Integer, LocalDate, String, Integer>> i = sql.fetch().iterator();
Map<LocalDate, SlotBean> map = new HashMap<>();
while (i.hasNext()) {
Record4<Integer, LocalDate, String, Integer> n = i.next();
LocalDate day = n.get(T_REQUIRED_WORKTIME.DAY);
Integer pk = n.get(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME);
Integer minutes = n.get(T_REQUIRED_WORKTIME.REQUIRED_MINUTES);
String reason = n.get(T_REQUIRED_WORKTIME.REASON);
map.put(day, SlotBean.of(pk, day, minutes, reason));
}
return map;
}
/**
* get slot if login fits
*
* @param id the ID of the slot
* @param login the login
* @return the slot or null
*/
public SlotBean getSlot(Integer id, String login) {
SelectConditionStep<Record4<Integer, LocalDate, String, Integer>> sql = jooq
// @formatter:off
.select(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME,
T_REQUIRED_WORKTIME.DAY,
T_REQUIRED_WORKTIME.REASON,
T_REQUIRED_WORKTIME.REQUIRED_MINUTES)
.from(T_REQUIRED_WORKTIME)
.innerJoin(T_LOGIN).on(T_LOGIN.PK.eq(T_REQUIRED_WORKTIME.FK_LOGIN))
.where(T_LOGIN.LOGIN.eq(login))
.and(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME.eq(id));
// @formatter:on
LOGGER.trace(sql);
Record4<Integer, LocalDate, String, Integer> r = sql.fetchOne();
return r == null ? null
: SlotBean.of(r.get(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME), r.get(T_REQUIRED_WORKTIME.DAY),
r.get(T_REQUIRED_WORKTIME.REQUIRED_MINUTES), r.get(T_REQUIRED_WORKTIME.REASON));
}
private String nullIfEmpty(String s) {
return s == null ? null : (s.isBlank() ? null : s);
}
public void addSlotRange(Integer minutes, String login, String reason, List<LocalDate> days) {
Integer fkLogin = jooq.select(T_LOGIN.PK).from(T_LOGIN).where(T_LOGIN.LOGIN.eq(login)).fetchOne(T_LOGIN.PK);
List<Row4<LocalDate, Integer, String, Integer>> rows = new ArrayList<Row4<LocalDate,Integer,String,Integer>>();
for(LocalDate day : days) {
rows.add(DSL.row(day, minutes, nullIfEmpty(reason), fkLogin));
}
InsertReturningStep<TRequiredWorktimeRecord> sql = jooq
// @formatter:off
.insertInto(T_REQUIRED_WORKTIME,
T_REQUIRED_WORKTIME.DAY,
T_REQUIRED_WORKTIME.REQUIRED_MINUTES,
T_REQUIRED_WORKTIME.REASON,
T_REQUIRED_WORKTIME.FK_LOGIN)
.valuesOfRows(rows)
.onConflict(T_REQUIRED_WORKTIME.FK_LOGIN, T_REQUIRED_WORKTIME.DAY)
.doUpdate()
.setAllToExcluded();
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
public void addSlot(SlotBean bean, String login) {
InsertOnDuplicateSetMoreStep<TRequiredWorktimeRecord> sql = jooq
// @formatter:off
.insertInto(T_REQUIRED_WORKTIME,
T_REQUIRED_WORKTIME.DAY,
T_REQUIRED_WORKTIME.REQUIRED_MINUTES,
T_REQUIRED_WORKTIME.REASON,
T_REQUIRED_WORKTIME.FK_LOGIN)
.select(jooq
.select(DSL.val(bean.getDay()), DSL.val(bean.getMinutes()), DSL.val(nullIfEmpty(bean.getReason())), T_LOGIN.PK)
.from(T_LOGIN)
.where(T_LOGIN.LOGIN.eq(login)))
.onConflict(T_REQUIRED_WORKTIME.FK_LOGIN, T_REQUIRED_WORKTIME.DAY)
.doUpdate()
.set(T_REQUIRED_WORKTIME.REQUIRED_MINUTES, bean.getMinutes())
.set(T_REQUIRED_WORKTIME.REASON, nullIfEmpty(bean.getReason()));
// @formatter:off
LOGGER.trace(sql);
sql.execute();
}
public void updateSlot(SlotBean bean, String login) {
UpdateConditionStep<TRequiredWorktimeRecord> sql = jooq
// @formatter:off
.update(T_REQUIRED_WORKTIME)
.set(T_REQUIRED_WORKTIME.REQUIRED_MINUTES, bean.getMinutes())
.set(T_REQUIRED_WORKTIME.REASON, nullIfEmpty(bean.getReason()))
.where(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME.eq(bean.getId()))
.and(T_REQUIRED_WORKTIME.FK_LOGIN.in(jooq
.select(T_LOGIN.PK)
.from(T_LOGIN)
.where(T_LOGIN.LOGIN.eq(login))));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
public void deleteSlot(Integer id, String login) {
DeleteConditionStep<TRequiredWorktimeRecord> sql = jooq
// @formatter:off
.deleteFrom(T_REQUIRED_WORKTIME)
.where(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME.eq(id))
.and(T_REQUIRED_WORKTIME.FK_LOGIN.in(jooq
.select(T_LOGIN.PK)
.from(T_LOGIN)
.where(T_LOGIN.LOGIN.eq(login))));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
/**
* update the field only
*
* @param fkDone the ID
* @param value the value
* @param tableField the field
*/
public void updateField(Integer fkDone, Integer value, TableField<TDoneRecord, Integer> tableField) {
UpdateConditionStep<TDoneRecord> sql = jooq
// @formatter:off
.update(T_DONE)
.set(tableField, value)
.where(T_DONE.PK.eq(fkDone));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
}

View File

@ -1,8 +1,15 @@
package de.jottyfan.timetrack.modules.done;
import static de.jottyfan.timetrack.db.done.Tables.T_DONE;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -18,6 +25,11 @@ import de.jottyfan.timetrack.db.done.tables.records.VBillingRecord;
import de.jottyfan.timetrack.db.done.tables.records.VJobRecord;
import de.jottyfan.timetrack.db.done.tables.records.VModuleRecord;
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.done.model.SlotBean;
import de.jottyfan.timetrack.modules.note.NoteService;
/**
@ -29,6 +41,13 @@ 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 DoneRepository repository;
@Autowired
private DSLContext dsl;
@ -142,4 +161,184 @@ 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<DoneBean> 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);
}
public void favorize(Integer id) {
try {
new DoneGateway(dsl).favorize(id);
} catch (Exception e) {
}
}
public void unfavorize(Integer id) {
try {
new DoneGateway(dsl).unfavorize(id);
} catch (Exception e) {
}
}
public List<FavoriteBean> getFavorites(String username) {
try {
DoneGateway gw = new DoneGateway(dsl);
Integer login = gw.getUserId(username);
return gw.getFavorites(login);
} catch (Exception e) {
return new ArrayList<>();
}
}
public void usefavorite(Integer fkFavorite) {
try {
new DoneGateway(dsl).useFav(fkFavorite, timeService.roundTime(LocalDateTime.now(), INTERVAL));
} catch (Exception e) {
}
}
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());
}
public List<SlotBean> getSlots(LocalDate day, String username) {
YearMonth ym = YearMonth.from(day);
LocalDate from = ym.atDay(1);
LocalDate until = ym.atEndOfMonth();
Map<LocalDate, SlotBean> map = new HashMap<>();
LocalDate i = from;
while (i.isBefore(until.plusDays(1))) {
map.put(i, SlotBean.of(i));
i = i.plusDays(1);
}
map.putAll(repository.getSlots(from, until, username));
List<SlotBean> list = new ArrayList<>(map.values());
list.sort((o1, o2) -> {
return o1 == null || o2 == null || o1.getDay() == null ? 0 : o1.getDay().compareTo(o2.getDay());
});
return list;
}
public SlotBean getSlot(Integer id, String username) {
return repository.getSlot(id, username);
}
/**
* get a list of days until the 1st of the month starts in the calendar - start
* with sunday for the matrix
*
* @param day the day; only the month will be used
* @return a list of numbers
*/
public List<Integer> getSlotOffset(LocalDate day) {
List<Integer> list = new ArrayList<Integer>();
YearMonth ym = YearMonth.from(day);
LocalDate first = ym.atDay(1);
for (int i = 0; i < first.getDayOfWeek().getValue(); i++) {
list.add(i);
}
return list;
}
/**
* upsert the bean
*
* @param bean the bean
* @param username the username
*/
public void upsert(SlotBean bean, String username) {
if (bean.getId() == null) {
repository.addSlot(bean, username);
} else {
repository.updateSlot(bean, username);
}
}
public void delete(Integer slotId, String username) {
repository.deleteSlot(slotId, username);
}
public void addSlotRange(Integer minutes, LocalDate from, LocalDate until, String reason, String username, Boolean includeSaturdays, Boolean includeSundays) {
List<LocalDate> days = new ArrayList<>();
if (!from.isBefore(until)) {
LocalDate tmp = from;
from = until;
until = tmp;
}
includeSaturdays = includeSaturdays == null ? false : includeSaturdays;
includeSundays = includeSundays == null ? false : includeSundays;
for (LocalDate i = from; i.isBefore(until.plusDays(1)); i = i.plusDays(1)) {
if (i.getDayOfWeek().equals(DayOfWeek.SUNDAY) && !includeSundays) {
// ignore
} else if (i.getDayOfWeek().equals(DayOfWeek.SATURDAY) && !includeSaturdays) {
// ignore
} else {
days.add(i);
}
}
repository.addSlotRange(minutes, username, reason, days);
}
public void updateField(Integer fkDone, String field, Integer value) {
if ("project".equals(field)) {
repository.updateField(fkDone, value, T_DONE.FK_PROJECT);
} else if ("module".equals(field)) {
repository.updateField(fkDone, value, T_DONE.FK_MODULE);
} else if ("job".equals(field)) {
repository.updateField(fkDone, value, T_DONE.FK_JOB);
} else {
LOGGER.error("field {} not supported yet", field);
}
}
}

View File

@ -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).withSecond(0).withNano(0);
}
}
}

View File

@ -6,13 +6,11 @@ import org.springframework.ui.Model;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.done.tables.records.TJobRecord;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -22,13 +20,10 @@ import jakarta.annotation.security.RolesAllowed;
*
*/
@Controller
public class JobController {
public class JobController extends CommonController {
@Autowired
private JobService jobService;
@Autowired
private DoneController doneController;
@Autowired
private OAuth2Provider provider;
@ -37,7 +32,7 @@ public class JobController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/edit/job/{id}")
public String toJob(@PathVariable Integer id, Model model) {
public String toJob(@PathVariable("id") Integer id, Model model) {
String username = provider.getName();
TJobRecord job = jobService.get(id);
model.addAttribute("jobBean", job);
@ -46,22 +41,22 @@ public class JobController {
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/job", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute TJobRecord bean) {
@PostMapping("/done/upsert/job")
public String doUpsert(Model model, @ModelAttribute("bean") 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")
@RequestMapping(value = "/done/add/job", method = RequestMethod.GET)
@GetMapping("/done/add/job")
public String toAddJob(Model model) {
return toJob(null, model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/job/{id}")
public String doDeleteJob(@PathVariable Integer id, Model model) {
public String doDeleteJob(@PathVariable("id") 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);
}
}

View File

@ -0,0 +1,141 @@
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.LocalTime;
/**
*
* @author jotty
*
*/
public class DaysumBean implements Serializable {
private static final long serialVersionUID = 1L;
private LocalTime daytimeFrom;
private LocalTime daytimeUntil;
private LocalTime dayworktime;
private LocalTime breaks;
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
*/
public LocalTime getDaytimeFrom() {
return daytimeFrom;
}
/**
* @param daytimeFrom the daytimeFrom to set
*/
public void setDaytimeFrom(LocalTime daytimeFrom) {
this.daytimeFrom = daytimeFrom;
}
/**
* @return the daytimeUntil
*/
public LocalTime getDaytimeUntil() {
return daytimeUntil;
}
/**
* @param daytimeUntil the daytimeUntil to set
*/
public void setDaytimeUntil(LocalTime daytimeUntil) {
this.daytimeUntil = daytimeUntil;
}
/**
* @return the dayworktime
*/
public LocalTime getDayworktime() {
return dayworktime;
}
/**
* @param dayworktime the dayworktime to set
*/
public void setDayworktime(LocalTime dayworktime) {
this.dayworktime = dayworktime;
}
/**
* @return the breaks
*/
public LocalTime getBreaks() {
return breaks;
}
/**
* @param breaks the breaks to set
*/
public void setBreaks(LocalTime breaks) {
this.breaks = breaks;
}
/**
* @return the dayOvertime
*/
public Integer getDayOvertime() {
return dayOvertime;
}
/**
* @param dayOvertime the dayOvertime to set
*/
public void setDayOvertime(Integer dayOvertime) {
this.dayOvertime = dayOvertime;
}
/**
* @return the totalovertime
*/
public Integer getTotalOvertime() {
return totalOvertime;
}
/**
* @param totalovertime the totalovertime to set
*/
public void setTotalOvertime(Integer totalOvertime) {
this.totalOvertime = totalOvertime;
}
}

View File

@ -1,4 +1,4 @@
package de.jottyfan.timetrack.modules.done;
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.Duration;
@ -37,9 +37,11 @@ public class DoneBean implements Serializable, Comparable<DoneBean> {
private Integer fkModule;
private Integer fkJob;
private Integer fkBilling;
private Boolean isFavorite;
public DoneBean() {
this.day = null;
isFavorite = false;
}
public DoneBean(TDoneRecord r, Map<Integer, VProjectRecord> projectMap, Map<Integer, VModuleRecord> moduleMap,
@ -56,6 +58,7 @@ public class DoneBean implements Serializable, Comparable<DoneBean> {
this.fkModule = module.getPk();
this.fkJob = activity.getPk();
this.fkBilling = billing.getPk();
isFavorite = false;
}
private final String nullable(Object o, String format) {
@ -94,6 +97,7 @@ public class DoneBean implements Serializable, Comparable<DoneBean> {
buf.append(",module=").append(module == null ? "" : module.getName());
buf.append(",activity=").append(activity == null ? "" : activity.getName());
buf.append(",billing=").append(billing == null ? "" : billing.getName());
buf.append(",isFavorite=").append(isFavorite);
buf.append("}");
return buf.toString();
}
@ -383,4 +387,18 @@ public class DoneBean implements Serializable, Comparable<DoneBean> {
public void setFkBilling(Integer fkBilling) {
this.fkBilling = fkBilling;
}
/**
* @return the isFavorite
*/
public Boolean getIsFavorite() {
return isFavorite;
}
/**
* @param isFavorite the isFavorite to set
*/
public void setIsFavorite(Boolean isFavorite) {
this.isFavorite = isFavorite;
}
}

View File

@ -1,4 +1,4 @@
package de.jottyfan.timetrack.modules.done;
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.LocalDate;
@ -19,7 +19,7 @@ public class DoneModel implements Serializable {
private LocalDate day;
public DoneModel() {
this.day = LocalDate.now();
day = LocalDate.now();
}
public String getDayString() {

View File

@ -0,0 +1,88 @@
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
/**
*
* @author jotty
*
*/
public class FavoriteBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer fkFavorite;
private String project;
private String module;
private String job;
private String billing;
/**
* @return the project
*/
public String getProject() {
return project;
}
/**
* @param project the project to set
*/
public void setProject(String project) {
this.project = project;
}
/**
* @return the module
*/
public String getModule() {
return module;
}
/**
* @param module the module to set
*/
public void setModule(String module) {
this.module = module;
}
/**
* @return the job
*/
public String getJob() {
return job;
}
/**
* @param job the job to set
*/
public void setJob(String job) {
this.job = job;
}
/**
* @return the billing
*/
public String getBilling() {
return billing;
}
/**
* @param billing the billing to set
*/
public void setBilling(String billing) {
this.billing = billing;
}
/**
* @return the fkFavorite
*/
public Integer getFkFavorite() {
return fkFavorite;
}
/**
* @param fkFavorite the fkFavorite to set
*/
public void setFkFavorite(Integer fkFavorite) {
this.fkFavorite = fkFavorite;
}
}

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

@ -0,0 +1,102 @@
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 SlotBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
@DateTimeFormat(pattern="yyyy-MM-dd")
private LocalDate day;
private Integer minutes;
private String reason;
public static final SlotBean of(Integer id, LocalDate day, Integer minutes, String reason) {
SlotBean bean = new SlotBean();
bean.setId(id);
bean.setDay(day);
bean.setMinutes(minutes);
bean.setReason(reason);
return bean;
}
public static final SlotBean of(LocalDate day) {
SlotBean bean = new SlotBean();
bean.setDay(day);
return bean;
}
public String printTime() {
Integer hours = 0;
Integer mins = 0;
if (minutes != null) {
hours = minutes / 60;
mins = minutes % 60;
}
return String.format("%2d:%02d", hours, mins);
}
/**
* @return the day
*/
public LocalDate getDay() {
return day;
}
/**
* @param day the day to set
*/
public void setDay(LocalDate day) {
this.day = day;
}
/**
* @return the minutes
*/
public Integer getMinutes() {
return minutes;
}
/**
* @param minutes the minutes to set
*/
public void setMinutes(Integer minutes) {
this.minutes = minutes;
}
/**
* @return the id
*/
public Integer getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Integer id) {
this.id = id;
}
/**
* @return the reason
*/
public String getReason() {
return reason;
}
/**
* @param reason the reason to set
*/
public void setReason(String reason) {
this.reason = reason;
}
}

View File

@ -0,0 +1,104 @@
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.LocalDate;
/**
*
* @author jotty
*
*/
public class SlotRangeBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer minutes;
private LocalDate from;
private LocalDate until;
private String reason;
private Boolean includeSaturday;
private Boolean includeSunday;
/**
* @return the minutes
*/
public Integer getMinutes() {
return minutes;
}
/**
* @param minutes the minutes to set
*/
public void setMinutes(Integer minutes) {
this.minutes = minutes;
}
/**
* @return the from
*/
public LocalDate getFrom() {
return from;
}
/**
* @param from the from to set
*/
public void setFrom(LocalDate from) {
this.from = from;
}
/**
* @return the until
*/
public LocalDate getUntil() {
return until;
}
/**
* @param until the until to set
*/
public void setUntil(LocalDate until) {
this.until = until;
}
/**
* @return the reason
*/
public String getReason() {
return reason;
}
/**
* @param reason the reason to set
*/
public void setReason(String reason) {
this.reason = reason;
}
/**
* @return the includeSaturday
*/
public Boolean getIncludeSaturday() {
return includeSaturday;
}
/**
* @param includeSaturday the includeSaturday to set
*/
public void setIncludeSaturday(Boolean includeSaturday) {
this.includeSaturday = includeSaturday;
}
/**
* @return the includeSunday
*/
public Boolean getIncludeSunday() {
return includeSunday;
}
/**
* @param includeSunday the includeSunday to set
*/
public void setIncludeSunday(Boolean includeSunday) {
this.includeSunday = includeSunday;
}
}

View File

@ -1,4 +1,4 @@
package de.jottyfan.timetrack.modules.done;
package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.Duration;

View File

@ -6,13 +6,11 @@ import org.springframework.ui.Model;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.done.tables.records.TModuleRecord;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -22,14 +20,11 @@ import jakarta.annotation.security.RolesAllowed;
*
*/
@Controller
public class ModuleController {
public class ModuleController extends CommonController {
@Autowired
private ModuleService moduleService;
@Autowired
private DoneController doneController;
@Autowired
private OAuth2Provider provider;
@ -38,7 +33,7 @@ public class ModuleController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/edit/module/{id}")
public String toModule(@PathVariable Integer id, Model model) {
public String toModule(@PathVariable("id") Integer id, Model model) {
String username = provider.getName();
TModuleRecord module = moduleService.get(id);
model.addAttribute("moduleBean", module);
@ -47,22 +42,22 @@ public class ModuleController {
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/module", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute TModuleRecord bean) {
@PostMapping("/done/upsert/module")
public String doUpsert(Model model, @ModelAttribute("bean") 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")
@RequestMapping(value = "/done/add/module", method = RequestMethod.GET)
@GetMapping("/done/add/module")
public String toAddModule(Model model) {
return toModule(null, model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/module/{id}")
public String doDeleteModule(@PathVariable Integer id, Model model) {
public String doDeleteModule(@PathVariable("id") 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);
}
}

View File

@ -6,13 +6,11 @@ import org.springframework.ui.Model;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -22,13 +20,10 @@ import jakarta.annotation.security.RolesAllowed;
*
*/
@Controller
public class ProjectController {
public class ProjectController extends CommonController {
@Autowired
private ProjectService projectService;
@Autowired
private DoneController doneController;
@Autowired
private OAuth2Provider provider;
@ -37,7 +32,7 @@ public class ProjectController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/edit/project/{id}")
public String toProject(@PathVariable Integer id, Model model) {
public String toProject(@PathVariable("id") Integer id, Model model) {
String username = provider.getName();
TProjectRecord project = projectService.get(id);
model.addAttribute("projectBean", project);
@ -46,22 +41,22 @@ public class ProjectController {
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/project", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute TProjectRecord bean) {
@PostMapping("/done/upsert/project")
public String doUpsert(Model model, @ModelAttribute("bean") 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")
@RequestMapping(value = "/done/add/project", method = RequestMethod.GET)
@GetMapping("/done/add/project")
public String toAddProject(Model model) {
return toProject(null, model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/project/{id}")
public String doDeleteProject(@PathVariable Integer id, Model model) {
public String doDeleteProject(@PathVariable("id") 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);
}
}

View File

@ -10,13 +10,11 @@ import org.springframework.ui.Model;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.note.enums.EnumCategory;
import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import de.jottyfan.timetrack.modules.CommonController;
import jakarta.annotation.security.RolesAllowed;
/**
@ -25,37 +23,28 @@ import jakarta.annotation.security.RolesAllowed;
*
*/
@Controller
public class NoteController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
public class NoteController extends CommonController {
@Autowired
private NoteService noteService;
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/list")
@GetMapping("/note/list")
public String getList(Model model) {
String username = provider.getName();
List<NoteBean> list = noteService.getList();
model.addAttribute("noteList", list);
model.addAttribute("theme", profileService.getTheme(username));
return "note/list";
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/add", method = RequestMethod.GET)
@GetMapping("/note/add")
public String toAdd(Model model, OAuth2AuthenticationToken token) {
return toItem(null, model);
}
@RolesAllowed("timetrack_user")
@GetMapping("/note/edit/{id}")
public String toItem(@PathVariable Integer id, Model model) {
String username = provider.getName();
public String toItem(@PathVariable("id") Integer id, Model model) {
NoteBean bean = noteService.getBean(id);
if (bean == null) {
bean = new NoteBean(); // the add case
@ -63,20 +52,19 @@ public class NoteController {
model.addAttribute("noteBean", bean);
model.addAttribute("types", Arrays.asList(EnumNotetype.values()));
model.addAttribute("categories", Arrays.asList(EnumCategory.values()));
model.addAttribute("theme", profileService.getTheme(username));
return "note/item";
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/upsert", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute NoteBean bean, OAuth2AuthenticationToken token) {
@PostMapping("/note/upsert")
public String doUpsert(Model model, @ModelAttribute("bean") NoteBean bean, OAuth2AuthenticationToken token) {
Integer amount = noteService.doUpsert(bean);
return amount.equals(1) ? getList(model) : toItem(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/note/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) {
public String doDelete(@PathVariable("id") Integer id, Model model, OAuth2AuthenticationToken token) {
Integer amount = noteService.doDelete(id);
return amount.equals(1) ? getList(model) : toItem(id, model);
}

View File

@ -3,10 +3,9 @@ package de.jottyfan.timetrack.modules.profile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
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.PostMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.CommonController;
/**
*
@ -14,7 +13,7 @@ import de.jottyfan.timetrack.component.OAuth2Provider;
*
*/
@Controller
public class ProfileController {
public class ProfileController extends CommonController {
@Autowired
private OAuth2Provider provider;
@ -22,8 +21,8 @@ public class ProfileController {
@Autowired
private ProfileService service;
@RequestMapping(value = "/profile/{theme}", method = RequestMethod.POST)
public String setTheme(@PathVariable String theme) {
@PostMapping("/profile/{theme}")
public String setTheme(@PathVariable("theme") String theme) {
String username = provider.getName();
service.setTheme(username, theme);
return "redirect:/";

View File

@ -0,0 +1,29 @@
package de.jottyfan.timetrack.modules.style;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import de.jottyfan.timetrack.component.OAuth2Provider;
import jakarta.servlet.http.HttpServletRequest;
/**
*
* @author jotty
*
*/
@RestController
public class DynamicStyleController {
@Autowired
private DynamicStyleService service;
@Autowired
private OAuth2Provider provider;
@GetMapping(value = "/public/dynamicstyle.css", produces = "text/css")
public @ResponseBody String getDynamicCss(HttpServletRequest request) {
return service.getDynamicCssOf(provider.getName());
}
}

View File

@ -0,0 +1,36 @@
package de.jottyfan.timetrack.modules.style;
import static de.jottyfan.timetrack.db.profile.Tables.T_PROFILE;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.Record1;
import org.jooq.SelectConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
/**
*
* @author jotty
*
*/
@Repository
public class DynamicStyleRepository {
private static final Logger LOGGER = LogManager.getLogger(DynamicStyleRepository.class);
@Autowired
private DSLContext jooq;
public String getDynamicStyle(String name) {
SelectConditionStep<Record1<String>> sql = jooq
// @formatter:off
.select(T_PROFILE.DYNAMIC_CSS)
.from(T_PROFILE)
.where(T_PROFILE.USERNAME.eq(name));
// @formatter:on
LOGGER.trace(sql);
String result = sql.fetchOne(T_PROFILE.DYNAMIC_CSS);
return result == null ? "" : result;
}
}

View File

@ -0,0 +1,20 @@
package de.jottyfan.timetrack.modules.style;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* @author jotty
*
*/
@Service
public class DynamicStyleService {
@Autowired
private DynamicStyleRepository repository;
public String getDynamicCssOf(String name) {
return name == null ? "" : repository.getDynamicStyle(name);
}
}

View File

@ -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

View File

@ -42,6 +42,13 @@ body {
background-color: #222;
}
@media(min-width:1600px) {
.tabdivblurred {
margin: auto;
width: 1111px;
}
}
.tabdivblurred {
padding: 8px;
padding-bottom: 0px;
@ -203,6 +210,15 @@ body {
color: black !important;
}
.ES {
color: black;
background: radial-gradient(rgb(111, 255, 209), rgb(1, 113, 52)) !important;
}
[data-bs-theme="dark"] .ES {
color: black !important;
}
.left {
text-align: left;
}
@ -226,13 +242,17 @@ body {
.version {
font-size: small;
color: silver;
color: black;
position: absolute;
padding-top: 36px;
padding-left: 22px;
z-index: 0;
}
[data-bs-theme="dark"] .version {
color: white;
}
.fc-content {
cursor: pointer;
}
@ -264,39 +284,62 @@ body {
}
.emphgreen {
font-weight: bolder;
color: #136600;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
}
.emphblue {
font-weight: bolder;
color: #1a5fb4;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
}
.emphorange {
font-weight: bolder;
color: #c64600;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
}
.emphred {
font-weight: bolder;
color: #a51d2d;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white);
}
.emphpink {
color: #613583;
background-image: linear-gradient(to left, #e6e6e6, white);
}
.emphgray {
color: #5e5c64;
background-image: linear-gradient(to left, #959595, #e6e6e6);
}
.unround-border {
padding: 4px;
font-weight: bolder;
}
.border-frame {
border: 1px solid;
}
.round-border {
border-radius: 8px;
font-weight: bolder;
padding: 4px;
}
.round-border-right {
font-weight: bolder;
padding: 4px;
border-radius: 0px 8px 8px 0px;
}
.sumfield {
border: 1px solid;
border-radius: 8px;
padding: 4px;
padding-right: 0px;
margin-right: 4px;
}
.tab-pane-table {
@ -358,3 +401,95 @@ body {
max-height: calc(90vh - 100px);
margin: 8px;
}
.btn-list {
padding-left: 8px;
padding-right: 8px;
padding-top: 4px;
padding-bottom: 4px;
border-radius: 4px;
border: 1px solid transparent;
}
.btn-list:hover {
border: 1px solid rgb(119, 118, 123);
color: white;
background-image: linear-gradient(to right bottom, #99c1f1, #1a5f64);
}
[data-bs-theme=dark] .btn-list:hover {
border: 1px solid rgb(246, 245, 244);
color: rgb(246, 245, 244);
background-image: linear-gradient(to right bottom, #99c1f1, #1a5f64);
}
.golden {
color: darkgoldenrod;
}
.slot_badge {
white-space: nowrap;
margin-bottom: 4px;
}
.slot_badge_left {
border: 1px solid silver;
border-radius: 12px 0px 0px 12px;
background-color: #ccc;
color: black;
padding-left: 2px;
padding-top: 2px;
padding-bottom: 2px;
}
[data-bs-theme=dark] .slot_badge_left {
background-color: gray;
}
.slot_badge_middle {
border-top: 1px solid silver;
border-bottom: 1px solid silver;
padding: 2px;
text-decoration: none;
}
.slot_badge_middle:hover {
color: white;
background-image: linear-gradient(to right bottom, #99c1f1, #1a5f64);
}
.slot_badge_right {
border: 1px solid silver;
border-radius: 0px 12px 12px 0px;
background-color: transparent;
color: black;
padding-right: 2px;
padding-top: 2px;
padding-bottom: 2px;
}
[data-bs-theme=dark] .slot_badge_right {
color: white;
}
.slot_reason {
color: #26a269;
}
[data-bs-theme=dark] .slot_reason {
color: lime;
}
.flex-row-weekday {
display: flex;
flex-flow: row wrap;
}
.row-weekday > .col {
flex: 0 1 calc(100%/7);
max-width: calc(100%/7);
}
.boldy {
font-weight: bolder;
}

View File

@ -1,8 +1,8 @@
toggleTheme = function() {
toggleTheme = function(urlprefix) {
var oldValue = $("html").attr("data-bs-theme");
var newValue = oldValue == "dark" ? "light" : "dark";
$("html").attr("data-bs-theme", newValue);
var url = "profile/" + newValue;
var url = urlprefix + "/profile/" + newValue;
$.ajax({
url: url,
dataType: 'json',

View File

@ -53,12 +53,16 @@ class Schedule {
}
time2pixel = function (time, hourHeight) {
if (time == null) {
return 0;
} else {
var timeArray = time.split(":");
var hours = parseInt(timeArray[0]);
var minutes = parseInt(timeArray[1]);
var pixels = parseInt((hours + (minutes / 60)) * hourHeight);
return pixels;
}
}
drawSlot = function(ctx, slotNr, from, until, color, fillColor) {
ctx.strokeStyle = color;

View File

@ -1,6 +1,5 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/main.html}">
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/main.html}">
<head>
<title>Arbeitszeit</title>
</head>
@ -8,7 +7,10 @@
<font layout:fragment="title">Arbeitszeit</font>
<ul layout:fragment="menuitem">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<form th:action="@{/done/list}" th:object="${doneModel}" method="post">
<a class="nav-link btn btn-primary btn-white-text" th:href="@{/done/list/previousday}" style="width: 50px; float: left;">
<i class="fa fa-chevron-left"></i>
</a>
<form th:action="@{/done/list}" th:object="${doneModel}" method="post" style="float: left;">
<div class="nav-link" style="padding-top: 5px !important; padding-bottom: 0px !important">
<div class="input-group input-group-sm mb-3" style="margin-bottom: 0px !important">
<input type="date" class="form-control" th:value="*{day}" th:field="*{day}" />
@ -16,30 +18,74 @@
</div>
</div>
</form>
<a class="nav-link btn btn-primary btn-white-text" th:href="@{/done/list/nextday}" style="width: 50px; float: left;">
<i class="fa fa-chevron-right"></i>
</a>
</li>
</ul>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<table>
<tr>
<td><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/{day}(day=${doneModel.day})}">Neuer
Eintrag</a></td>
<td style="padding-left: 8px"><a class="nav-link btn btn-bordered btn-dangerhover" style="width: 44px" th:href="@{/done/list}"><i class="fas fa-sync"></i></a></td>
<td><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/{day}(day=${doneModel.day})}">Neuer Eintrag</a></td>
<td>
<div class="dropdown">
<button class="btn btn-white-text dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Letzte Einträge</button>
<ul class="dropdown-menu">
<li th:each="recent : ${recentList}">
<a class="dropdown-item" th:href="@{/done/addrecent/{id}(id=${recent.pk})}"
th:text="${(recent.getJobName()!=null?recent.getJobName():'') + '@' + (recent.getProject()!=null?recent.getProject().getName():'') + (recent.getModule()!=null? ', ' + recent.getModule().getName() : '')}"></a>
</li>
</ul>
</div>
</td>
<td style="padding-left: 8px"><a class="nav-link btn-list" th:href="@{/done/list}">
<i class="fas fa-sync"></i>
</a></td>
</tr>
</table>
</li>
</ul>
<main layout:fragment="content">
<ul class="nav nav-tabs navback" role="tablist">
<li class="nav-item"><a class="nav-link navlinkstyle active" data-bs-toggle="tab" href="#div_list">Liste</a></li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_schedule">Kalender</a></li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_project">Projekt</a></li>
<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>
<ul id="worktimetabs" class="nav nav-tabs navback" role="tablist">
<li class="nav-item">
<a class="nav-link navlinkstyle active" data-bs-toggle="tab" href="#div_list">Liste</a>
</li>
<li class="nav-item">
<a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_schedule">Kalender</a>
</li>
<li class="nav-item">
<a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_project">Projekt</a>
</li>
<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">
<script th:inline="javascript">
function submitDropdown(field) {
const value = field.value;
const id = field.getAttribute("data-id");
const fld = field.getAttribute("data-field");
const url_prefix = /*[[@{/done/update/}]]*/ "#";
const url = url_prefix + id + "?field=" + fld + "&value=" + value;
window.location.href = url;
}
</script>
<table class="table table-striped table-condensed">
<thead>
<tr>
@ -49,44 +95,70 @@
<th>Modul</th>
<th>Aufgabe</th>
<th>Abrechnung</th>
<th></th>
<th>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Favoriten</button>
<ul class="dropdown-menu">
<li th:each="f : ${favorites}">
<a class="dropdown-item" th:href="@{/done/usefav/{id}(id=${f.fkFavorite})}">
<span th:text="${f.project} + ' ' + ${f.module} + ' ' + ${f.job}"></span>
</a>
</li>
</ul>
</div>
</th>
</tr>
</thead>
<tbody>
<tr th:each="done : ${doneList}">
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span th:text="${done.timeNote}"></span></a></td>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span th:text="${done.timeDiff}"></span></a></td>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span class="boldtext"
th:text="${done.project?.name}"></span></a></td>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span class="boldtext"
th:text="${done.module?.name}"></span></a></td>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span class="boldtext"
th:text="${done.activity?.name}"></span></a></td>
<td><span th:text="${done.billing.shortcut}" th:class="'billing ' + ${done.billing.csskey}"
th:if="${done.billing != null}"></span></td>
<td><a th:href="@{/done/edit/{id}(id=${done.pk})}" th:title="${done.pk}"><i class="fa fa-edit"></i></a></td>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}">
<span th:text="${done.timeNote}"></span>
</a> <a th:if="${done.timeUntil == null}" style="margin-left: 4px" class="btn-list" th:href="@{/done/end/{id}(id=${done.pk})}" title="aktuelle Uhrzeit setzen">
<i class="fa fa-clock"></i>
</a></td>
<td><span th:text="${done.timeDiff}"></span></td>
<td>
<select onchange="submitDropdown(this)" th:data-id="${done.pk}" data-field="project">
<option value="">---</option>
<option th:each="p : ${projectList}" th:value="${p.pk}" th:text="${p.name}" th:selected="${done.project?.name == p.name ? 'selected' : 'false'}"></option>
</select>
</td>
<td>
<select onchange="submitDropdown(this)" th:data-id="${done.pk}" data-field="module">
<option value="">---</option>
<option th:each="m : ${moduleList}" th:value="${m.pk}" th:text="${m.name}" th:selected="${done.module?.name == m.name ? 'selected' : 'false'}"></option>
</select>
</td>
<td>
<select onchange="submitDropdown(this)" th:data-id="${done.pk}" data-field="job">
<option value="">---</option>
<option th:each="j : ${jobList}" th:value="${j.pk}" th:text="${j.name}" th:selected="${done.activity?.name == j.name ? 'selected' : 'false'}"></option>
</select>
</td>
<td><span th:text="${done.billing.shortcut}" th:class="'billing ' + ${done.billing.csskey}" th:if="${done.billing != null}"></span></td>
<td><a class="btn-list" th:href="@{/done/copy/{id}(id=${done.pk})}" title="Aufgabe neu beginnen"><i class="fa fa-copy"></i></a>
<a class="btn-list" th:href="@{/done/edit/{id}(id=${done.pk})}" title="Eintrag bearbeiten"><i class="fa fa-edit"></i></a>
<a class="btn-list" th:href="@{/done/favorize/{id}(id=${done.pk})}" title="als Favorit speichern" th:if="${!done.isFavorite}"><i class="far fa-star golden"></i></a>
<a class="btn-list" th:href="@{/done/unfavorize/{id}(id=${done.pk})}" title="Favoritenstatus entfernen" th:if="${done.isFavorite}"><i class="fas fa-star golden"></i></a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Zusammenfassung</td>
<td>Start: <span class="emphgreen" th:text="${sum.start}"></span></td>
<td>Ende: <span class="emphgreen" th:text="${sum.end}"></span></td>
<td>Arbeitszeit total: <span class="emphblue" th:text="${sum.total}"></span></td>
<td>Pausezeit total: <span class="emphorange" th:text="${sum.pause}"></span></td>
<td>Überstunden: <span class="emphred" th:text="${sum.overdue}"></span></td>
<tr th:if="${daysum}">
<td colspan="7"><span class="sumfield">Start: <span class="emphgreen round-border-right" th:text="${#temporals.format(daysum.daytimeFrom, 'HH:mm')}" th:if="${daysum.daytimeFrom}"></span></span>
<span class="sumfield">Ende: <span class="emphgreen round-border-right" th:text="${#temporals.format(daysum.daytimeUntil, 'HH:mm')}" th:if="${daysum.daytimeUntil}"></span></span>
<span class="sumfield">Tagessumme: <span class="emphblue unround-border" th:text="${#temporals.format(daysum.dayworktime, 'HH:mm')}" th:if="${daysum.dayworktime}"></span><span class="emphgray round-border-right" th:text="'/ ' + ${sumtime}"></span></span>
<span class="sumfield">Pausezeit total: <span class="emphorange round-border-right" th:text="${#temporals.format(daysum.breaks, 'HH:mm')}" th:if="${daysum.breaks}"></span></span>
<span class="sumfield">Überstunden heute: <span class="emphred round-border-right" th:text="${daysum.printDayOvertime()}"></span></span>
<span class="sumfield">Überstunden total: <span class="emphpink round-border-right" th:text="${daysum.printTotalOvertime()}"></span></span>
</td>
</tr>
<tr>
<td></td>
<td><span th:if="${sum.getBillingTime('WP2') != '0,0 h'}"><span class="billing WP2">WP2</span><span
th:text="${sum.getBillingTime('WP2')}" class="distfat"></span></span></td>
<td><span th:if="${sum.getBillingTime('WP4') != '0,0 h'}"><span class="billing WP4">WP4</span><span
th:text="${sum.getBillingTime('WP4')}" class="distfat"></span></span></td>
<td><span th:if="${sum.getBillingTime('WP5') != '0,0 h'}"><span class="billing WP5">WP5</span><span
th:text="${sum.getBillingTime('WP5')}" class="distfat"></span></span></td>
<td><span th:if="${sum.getBillingTime('TA3') != '0,0 h'}"><span class="billing TA3">TA3</span><span
th:text="${sum.getBillingTime('TA3')}" class="distfat"></span></span></td>
<td><span class="billing">X</span><span th:text="${sum.getBillingTime(null)}" class="distfat"></span></td>
<td><span th:if="${sum.getBillingTime('WP2') != '0,0 h'}"><span class="billing WP2">WP2</span><span th:text="${sum.getBillingTime('WP2')}" class="distfat"></span></span></td>
<td><span th:if="${sum.getBillingTime('WP4') != '0,0 h'}"><span class="billing WP4">WP4</span><span th:text="${sum.getBillingTime('WP4')}" class="distfat"></span></span></td>
<td><span th:if="${sum.getBillingTime('WP5') != '0,0 h'}"><span class="billing WP5">WP5</span><span th:text="${sum.getBillingTime('WP5')}" class="distfat"></span></span></td>
<td><span th:if="${sum.getBillingTime('TA3') != '0,0 h'}"><span class="billing TA3">TA3</span><span th:text="${sum.getBillingTime('TA3')}" class="distfat"></span></span></td>
<td colspan="2"><span class="billing">X</span><span th:text="${sum.getBillingTime(null)}" class="distfat"></span></td>
</tr>
</tfoot>
</table>
@ -107,13 +179,14 @@
<tr th:each="project : ${projectList}">
<td><span th:text="${project.name}"></span></td>
<td><span th:text="${project.percentUsage}"></span></td>
<td><a th:href="@{/done/edit/project/{id}(id=${project.pk})}" th:title="${project.pk}"><i class="fa fa-edit"></i></a></td>
<td><a th:href="@{/done/edit/project/{id}(id=${project.pk})}" th:title="${project.pk}">
<i class="fa fa-edit"></i>
</a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3"><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/project}">neues Projekt</a>
</td>
<td colspan="3"><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/project}">neues Projekt</a></td>
</tr>
</tfoot>
</table>
@ -131,7 +204,9 @@
<tr th:each="module : ${moduleList}">
<td><span th:text="${module.name}"></span></td>
<td><span th:text="${module.percentUsage}"></span></td>
<td><a th:href="@{/done/edit/module/{id}(id=${module.pk})}" th:title="${module.pk}"><i class="fa fa-edit"></i></a></td>
<td><a th:href="@{/done/edit/module/{id}(id=${module.pk})}" th:title="${module.pk}">
<i class="fa fa-edit"></i>
</a></td>
</tr>
</tbody>
<tfoot>
@ -154,7 +229,9 @@
<tr th:each="job : ${jobList}">
<td><span th:text="${job.name}"></span></td>
<td><span th:text="${job.percentUsage}"></span></td>
<td><a th:href="@{/done/edit/job/{id}(id=${job.pk})}" th:title="${job.pk}"><i class="fa fa-edit"></i></a></td>
<td><a th:href="@{/done/edit/job/{id}(id=${job.pk})}" th:title="${job.pk}">
<i class="fa fa-edit"></i>
</a></td>
</tr>
</tbody>
<tfoot>
@ -182,14 +259,108 @@
</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. Die Überstundenberechnung hängt von der
Vollständigkeit der vorhandenen Slots ab; fehlen Slots, wird die Arbeitszeit jener Tage nicht eingerechnet.<br /> Hier werden nur die Slots für diesen Monat angezeigt.
</div>
<div class="container">
<div class="row row-weekday">
<div class="col">Sonntag</div>
<div class="col boldy">Montag</div>
<div class="col boldy">Dienstag</div>
<div class="col boldy">Mittwoch</div>
<div class="col boldy">Donnerstag</div>
<div class="col boldy">Freitag</div>
<div class="col">Samstag</div>
</div>
<div class="row row-weekday">
<div class="col slot_badge" th:each="o : ${slotOffset}"></div>
<div class="col slot_badge" th:each="s : ${slots}">
<span class="slot_badge_left" th:text="${#temporals.format(s.day, 'dd.MM.')}"></span><a th:href="@{/done/slot/{id}(id=${s.id})}" class="slot_badge_middle" th:if="${s.id}">
<i class="fas fa-pencil"></i>
</a><a th:href="@{/done/slot/add?day={d}(d=${s.day})}" class="slot_badge_middle" th:unless="${s.id}">
<i class="fas fa-plus"></i>
</a>
<span class="slot_badge_middle slot_reason" th:if="${s.reason}" th:text="${s.reason}"></span><span th:class="${s.reason != null ? 'slot_badge_right' : 'slot_badge_right boldy'}"
th:text="${s.printTime()}" th:if="${s.id}"></span><span class="slot_badge_right" th:unless="${s.id}">&nbsp;--:--&nbsp;</span>
</div>
</div>
<br />
<div class="row">
<div class="col-2"><a th:href="@{/done/slot/back}" class="btn btn-outline-primary">&lt;- zurück</a></div>
<div class="col-8">
<a th:href="@{/done/slot/range}" class="btn btn-outline-primary">mehrere Slots auf einmal anlegen</a>
</div>
<div class="col-2"><a th:href="@{/done/slot/forward}" class="btn btn-outline-primary">weiter -&gt;</a></div>
</div>
<br />
<div class="row alert alert-info">
<div class="col-sm-12">
<span style="text-decoration: underline">Legende</span>
</div>
<div class="col">
Üb: Überstunden, Mehrarbeit<br /> Ur: Urlaub, Sonderurlaub, Kur<br /> gF: gesetzlicher Feiertag<br />
</div>
<div class="col">
Kr: Arbeits- und Dienstunfähigkeit<br /> Gl: Freistellung aus Gleitzeitguthaben<br /> Ar: Arbeits- und Dienstbefreiung<br />
</div>
<div class="col">
mK: "mit Kind krank"<br /> Di: Dienstreise, Dienstgänge<br />
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
let width = parseInt($("#schedule").css("min-width"));
let height = parseInt($("#schedule").css("min-height"));
var schedule = new Schedule("#schedule", width, height);
var ctx = $("#scheduleCanvas")[0].getContext("2d");
var currentDayRecords = JSON.parse('[(${schedule})]');
$(document)
.ready(
function() {
// the tab deeplink functionality
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
$('#worktimetabs a[href="#' + hash[1] + '"]').tab("show");
url = location.href.replace(/\/#/, "#");
history.replaceState(null, null, url);
}
// the schedule
let width = parseInt($("#schedule").css(
"min-width"));
let height = parseInt($("#schedule").css(
"min-height"));
var schedule = new Schedule("#schedule", width,
height);
var ctx = $("#scheduleCanvas")[0]
.getContext("2d");
var currentDayRecords = JSON
.parse('[(${schedule})]');
var scheduleRecords = currentDayRecords.schedule;
for (var i = 0; i < scheduleRecords.length; i++) {
var r = scheduleRecords[i];
@ -205,7 +376,9 @@
color = "#00aa00";
}
/* daySlot 7 = sunday, but this should be slot 0 */
schedule.drawSlot(ctx, r.daySlot > 6 ? 0 : r.daySlot, r.from, r.until, "black", color);
schedule.drawSlot(ctx, r.daySlot > 6 ? 0
: r.daySlot, r.from, r.until,
"black", color);
}
var localeUrl = '[[@{/js/dataTables/de.json}]]';
$("#project_table").DataTable({

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/main.html}">
<head>
<title>Slot aktualisieren</title>
</head>
<body>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="nav-link btn btn-outline-primary btn-white-text" th:href="@{/done/list#div_slot}">zur Slotübersicht</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container formpane">
<form th:action="@{/done/slot/upsert}" method="post" th:object="${bean}">
<input type="hidden" th:field="*{id}" />
<div class="row g-2" th:if="${bean}">
<div class="col-sm-3">Tag</div>
<div class="col-sm-9">
<input type="date" th:field="*{day}" class="form-control" />
</div>
<div class="col-sm-3">vereinbarte Arbeitszeit in Minuten</div>
<div class="col-sm-9">
<input type="number" th:field="*{minutes}" class="form-control">
</div>
<div class="col-sm-3">Abweichungsgrund</div>
<div class="col-sm-9">
<select th:field="*{reason}" class="form-select">
<option value="">-</option>
<option value="Ar">Arbeits- und Dienstbefreiung</option>
<option value="Di">Dienstreise, Dienstgänge</option>
<option value="gF">gesetzlicher Feiertag</option>
<option value="Gl">Freistellung aus Gleitzeitguthaben</option>
<option value="Kr">Arbeits- und Dienstunfähigkeit</option>
<option value="mK">"mit Kind krank"</option>
<option value="Ur">Urlaub, Sonderurlaub, Kur</option>
<option value="Üb">Überstunden, Mehrarbeit</option>
</select>
</div>
<div class="col-sm-3"></div>
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-primary">Übernehmen</button>
</div>
</div>
</form>
</div>
<div class="container formpane" th:if="${bean.id}">
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">Slot löschen</button>
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteModalLabel">Slot löschen</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body text-danger">
Wollen Sie die angegebene Arbeitszeit von <span th:text="${bean.printTime()}"></span> vom Tag <span th:text="${#temporals.format(bean.day, 'dd.MM.yyyy')}"></span> wirklich löschen?
</div>
<div class="modal-footer">
<a th:href="@{/done/slot/{id}/delete(id=${bean.id})}" class="btn btn-outline-danger">Ja</a>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Nein</button>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/main.html}">
<head>
<title>Slot aktualisieren</title>
</head>
<body>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="nav-link btn btn-outline-primary btn-white-text" th:href="@{/done/list#div_slot}">zur Slotübersicht</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container formpane">
<form th:action="@{/done/slot/addrange}" method="post" th:object="${bean}">
<div class="row g-2" th:if="${bean}">
<div class="col-sm-3">ab</div>
<div class="col-sm-9">
<input type="date" th:field="*{from}" class="form-control" />
</div>
<div class="col-sm-3">bis</div>
<div class="col-sm-9">
<input type="date" th:field="*{until}" class="form-control" />
</div>
<div class="col-sm-3">vereinbarte Arbeitszeit in Minuten</div>
<div class="col-sm-9">
<input type="number" th:field="*{minutes}" class="form-control">
</div>
<div class="col-sm-3">Abweichungsgrund</div>
<div class="col-sm-9">
<select th:field="*{reason}" class="form-select">
<option value="">-</option>
<option value="Ar">Arbeits- und Dienstbefreiung</option>
<option value="Di">Dienstreise, Dienstgänge</option>
<option value="gF">gesetzlicher Feiertag</option>
<option value="Gl">Freistellung aus Gleitzeitguthaben</option>
<option value="Kr">Arbeits- und Dienstunfähigkeit</option>
<option value="mK">"mit Kind krank"</option>
<option value="Ur">Urlaub, Sonderurlaub, Kur</option>
<option value="Üb">Überstunden, Mehrarbeit</option>
</select>
</div>
<div class="col-sm-3">inklusive Samstage</div>
<div class="col-sm-9">
<input type="checkbox" th:checked="*{includeSaturday}" name="includeSaturday" />
</div>
<div class="col-sm-3">inklusive Sonntage</div>
<div class="col-sm-9">
<input type="checkbox" th:checked="*{includeSunday}" name="includeSunday" />
</div>
<div class="col-sm-3"></div>
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-primary">Anlegen</button>
</div>
</div>
</form>
</div>
</main>
</body>
</html>

View File

@ -5,19 +5,19 @@
<title>Timetrack</title>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/5.3.1/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/datatables/1.13.5/css/dataTables.bootstrap5.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/font-awesome/6.4.2/css/all.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/fullcalendar/5.11.3/main.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/5.3.3/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/datatables/2.1.8/css/dataTables.dataTables.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/font-awesome/6.7.2/css/all.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/fullcalendar/6.1.9/main.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
<link rel="stylesheet" type="text/css" th:href="@{/public/dynamicstyle.css}">
<link rel="icon" type="image/png" sizes="32x32" th:href="@{/png/favicon/favicon-32x32.png}" />
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/png/favicon/favicon-16x16.png}" />
<script th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
<script th:src="@{/webjars/bootstrap/5.3.1/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/webjars/datatables/1.13.5/js/jquery.dataTables.min.js}"></script>
<script th:src="@{/webjars/datatables/1.13.5/js/dataTables.bootstrap5.min.js}"></script>
<script th:src="@{/webjars/fullcalendar/5.11.3/main.js}"></script>
<script th:src="@{/webjars/bootstrap/5.3.3/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/webjars/datatables/2.1.8/js/dataTables.dataTables.min.js}"></script>
<script th:src="@{/webjars/fullcalendar/6.1.9/main.js}"></script>
<script th:src="@{/js/helper.js}"></script>
<script th:src="@{/js/clock.js}"></script>
<script th:src="@{/js/schedule.js}"></script>
@ -47,7 +47,7 @@
<li class="nav-item"><a class="nav-link titlemod"><font layout:fragment="title"></font></a></li>
<li layout:fragment="menuitem" style="list-style-type: none"></li>
<li layout:fragment="menu" style="list-style-type: none"></li>
<li class="nav-item ms-auto"><div id="clock" class="clock" onclick="toggleTheme();resetClock()"></div></li>
<li class="nav-item ms-auto"><div id="clock" class="clock" th:attr="onclick=|toggleTheme('${baseUrl}');resetClock()|"></div></li>
</ul>
</div>
</div>

View File

@ -17,15 +17,15 @@
<div class="container">
<div class="row">
<div class="col-8"><span class="spanlabel">Start:</span></div>
<div class="col-4"><span class="emphgreen" th:text="${sum.start}"></span></div>
<div class="col-4"><span class="emphgreen round-border border-frame" th:text="${sum.start}"></span></div>
<div class="col-8"><span class="spanlabel">Ende:</span></div>
<div class="col-4"><span class="emphgreen" th:text="${sum.end}"></span></div>
<div class="col-4"><span class="emphgreen round-border border-frame" th:text="${sum.end}"></span></div>
<div class="col-8"><span class="spanlabel">Arbeitszeit total:</span></div>
<div class="col-4"><span class="emphblue" th:text="${sum.total}"></span></div>
<div class="col-4"><span class="emphblue round-border border-frame" th:text="${sum.total}"></span></div>
<div class="col-8"><span class="spanlabel">Pausezeit total:</span></div>
<div class="col-4"><span class="emphorange" th:text="${sum.pause}"></span></div>
<div class="col-4"><span class="emphorange round-border border-frame" th:text="${sum.pause}"></span></div>
<div class="col-8"><span class="spanlabel">Überstunden:</span></div>
<div class="col-4"><span class="emphred" th:text="${sum.overdue}"></span></div>
<div class="col-4"><span class="emphred round-border border-frame" th:text="${sum.overdue}"></span></div>
</div>
</div>
</div>

View File

@ -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().withSecond(0).withNano(0);
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")));
}
}