Compare commits

...

18 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
29 changed files with 941 additions and 328 deletions

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.4.3'
version = '1.5.7'
description = """timetrack"""
@ -23,28 +23,26 @@ repositories {
}
dependencies {
implementation 'de.jottyfan:timetrackjooq:20240104b'
implementation 'de.jottyfan:timetrackjooq:20240109'
implementation 'org.apache.logging.log4j:log4j-api:latest.release'
implementation 'org.apache.logging.log4j:log4j-core:latest.release'
implementation 'org.apache.logging.log4j:log4j-to-slf4j:latest.release'
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.1'
implementation 'org.webjars:font-awesome:6.4.2'
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:latest.release';
implementation 'org.webjars.bowergithub.datatables:datatables:1.10.21'
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'
@ -67,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

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

@ -1,7 +1,11 @@
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
@ -9,6 +13,12 @@ import org.springframework.web.bind.annotation.ModelAttribute;
*/
public abstract class CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@Value("${server.servlet.context-path}")
private String contextPath;
@ -16,4 +26,10 @@ public abstract class CommonController {
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.DoneService;
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 de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -39,9 +37,6 @@ public class IndexController extends CommonController {
@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 extends CommonController {
}
@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,9 +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.CommonController;
import de.jottyfan.timetrack.modules.profile.ProfileService;
/**
*
@ -17,19 +15,12 @@ import de.jottyfan.timetrack.modules.profile.ProfileService;
@Controller
public class CalendarController extends CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@Autowired
private CalendarService service;
@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,14 +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.CommonController;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
/**
@ -28,12 +25,6 @@ import jakarta.annotation.security.RolesAllowed;
@Controller
public class ContactController extends CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@Autowired
private ContactService contactService;
@ -44,43 +35,41 @@ public class ContactController extends CommonController {
}
@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

@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import de.jottyfan.timetrack.component.OAuth2Provider;
@ -19,9 +20,11 @@ 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 de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.websocket.server.PathParam;
/**
*
@ -35,9 +38,6 @@ public class DoneController extends CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@Autowired
private DoneService doneService;
@ -57,6 +57,11 @@ public class DoneController extends CommonController {
SummaryBean sumBean = new SummaryBean(list, day, maxWorkTime);
SummaryBean weekBean = new SummaryBean(week, day, maxWorkTime);
model.addAttribute("doneList", list);
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));
@ -68,47 +73,49 @@ public class DoneController extends CommonController {
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")
@PostMapping("/done/list")
public String getListForDate(Model model, @ModelAttribute("day") LocalDate day) {
DoneModel doneModel = new DoneModel();
doneModel.setDay(day);
public String getListForDate(Model model, @ModelAttribute("doneModel") DoneModel doneModel) {
return getList(model, doneModel);
}
@RolesAllowed("timetrack_user")
@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 String day, Model model) {
public String abort(@PathVariable("day") String day, Model model) {
return "redirect:/done/list";
}
@RolesAllowed("timetrack_user")
@GetMapping("/done/add/{day}")
public String toAdd(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate day, Model model) {
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();
model.addAttribute("doneBean", bean);
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
@ -118,7 +125,7 @@ public class DoneController extends CommonController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/end/{id}")
public String end(@PathVariable Integer id, Model model) {
public String end(@PathVariable("id") Integer id, Model model) {
DoneBean bean = doneService.getBean(id);
String username = provider.getName();
doneService.endToNow(bean, username);
@ -127,7 +134,7 @@ public class DoneController extends CommonController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/copy/{id}")
public String copyFromNow(@PathVariable Integer id, Model model) {
public String copyFromNow(@PathVariable("id") Integer id, Model model) {
DoneBean bean = doneService.getBean(id);
String username = provider.getName();
doneService.copyFromNow(bean, username);
@ -136,7 +143,7 @@ public class DoneController extends CommonController {
@RolesAllowed("timetrack_user")
@PostMapping("/done/upsert")
public String doUpsert(Model model, @ModelAttribute DoneBean bean) {
public String doUpsert(Model model, @ModelAttribute("bean") DoneBean bean) {
String username = provider.getName();
Integer amount = doneService.doUpsert(bean, username);
return amount.equals(1) ? "redirect:/done/list" : "redirect:/" + toItem(bean.getPk(), model);
@ -144,7 +151,7 @@ public class DoneController extends CommonController {
@RolesAllowed("timetrack_user")
@GetMapping("/done/addrecent/{id}")
public String addRecent(Model model, @PathVariable Integer id) {
public String addRecent(Model model, @PathVariable("id") Integer id) {
String username = provider.getName();
DoneBean bean = doneService.getBean(id);
doneService.addRecent(bean, username);
@ -169,28 +176,28 @@ public class DoneController extends CommonController {
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model) {
public String doDelete(@PathVariable("id") Integer id, Model model) {
Integer amount = doneService.doDelete(id);
return amount.equals(1) ? "redirect:/done/list" : "redirect:/" + toItem(id, model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/done/favorize/{id}")
public String favorize(@PathVariable Integer 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 Integer 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 Integer id) {
public String usefavorite(@PathVariable("id") Integer id) {
doneService.usefavorite(id);
return "redirect:/done/list";
}
@ -210,4 +217,56 @@ public class DoneController extends CommonController {
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,5 +1,6 @@
package de.jottyfan.timetrack.modules.done;
import static de.jottyfan.timetrack.db.done.Tables.T_DONE;
import static de.jottyfan.timetrack.db.done.Tables.T_OVERTIME;
import static de.jottyfan.timetrack.db.done.Tables.T_REQUIRED_WORKTIME;
import static de.jottyfan.timetrack.db.done.Tables.V_DAY;
@ -9,29 +10,39 @@ 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;
@ -87,12 +98,12 @@ public class DoneRepository {
Field<Integer> OVERTIME = DSL.field("overtime", Integer.class);
SelectHavingStep<Record1<Integer>> sql = jooq
// @formatter:off
.select(T_OVERTIME.OVERTIME_MINUTES.plus(DSL.sum(DSL.extract(V_DAY.WORKTIME, DatePart.MINUTE).minus(T_REQUIRED_WORKTIME.REQUIRED_MINUTES))).as(OVERTIME))
.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.ge(V_DAY.DAY.cast(LocalDateTime.class)))
.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);
@ -157,10 +168,11 @@ public class DoneRepository {
}
public Map<LocalDate, SlotBean> getSlots(LocalDate from, LocalDate until, String login) {
SelectSeekStep1<Record3<Integer, LocalDate, Integer>, LocalDate> sql = jooq
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))
@ -170,14 +182,15 @@ public class DoneRepository {
.orderBy(T_REQUIRED_WORKTIME.DAY);
// @formatter:on
LOGGER.trace(sql);
Iterator<Record3<Integer, LocalDate, Integer>> i = sql.fetch().iterator();
Iterator<Record4<Integer, LocalDate, String, Integer>> i = sql.fetch().iterator();
Map<LocalDate, SlotBean> map = new HashMap<>();
while (i.hasNext()) {
Record3<Integer, LocalDate, Integer> n = i.next();
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);
map.put(day, SlotBean.of(pk, day, minutes));
String reason = n.get(T_REQUIRED_WORKTIME.REASON);
map.put(day, SlotBean.of(pk, day, minutes, reason));
}
return map;
}
@ -190,10 +203,11 @@ public class DoneRepository {
* @return the slot or null
*/
public SlotBean getSlot(Integer id, String login) {
SelectConditionStep<Record3<Integer, LocalDate, Integer>> sql = jooq
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))
@ -201,9 +215,105 @@ public class DoneRepository {
.and(T_REQUIRED_WORKTIME.PK_REQUIRED_WORKTIME.eq(id));
// @formatter:on
LOGGER.trace(sql);
Record3<Integer, LocalDate, Integer> r = sql.fetchOne();
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.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,5 +1,8 @@
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;
@ -287,4 +290,55 @@ public class DoneService {
}
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

@ -6,14 +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.CommonController;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.model.DoneModel;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -27,9 +24,6 @@ public class JobController extends CommonController {
@Autowired
private JobService jobService;
@Autowired
private DoneController doneController;
@Autowired
private OAuth2Provider provider;
@ -38,7 +32,7 @@ public class JobController extends CommonController {
@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);
@ -47,21 +41,21 @@ public class JobController extends CommonController {
}
@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) ? "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) ? "redirect:/done/list" : toJob(id, model);
}

View File

@ -3,6 +3,8 @@ package de.jottyfan.timetrack.modules.done.model;
import java.io.Serializable;
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
/**
*
* @author jotty
@ -12,14 +14,17 @@ 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) {
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;
}
@ -80,4 +85,18 @@ public class SlotBean implements Serializable {
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

@ -6,14 +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.CommonController;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.model.DoneModel;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -28,9 +25,6 @@ public class ModuleController extends CommonController {
@Autowired
private ModuleService moduleService;
@Autowired
private DoneController doneController;
@Autowired
private OAuth2Provider provider;
@ -39,7 +33,7 @@ public class ModuleController extends CommonController {
@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);
@ -48,21 +42,21 @@ public class ModuleController extends CommonController {
}
@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) ? "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) ? "redirect:/done/list" : toModule(id, model);
}

View File

@ -6,14 +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.CommonController;
import de.jottyfan.timetrack.modules.done.DoneController;
import de.jottyfan.timetrack.modules.done.model.DoneModel;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
@ -27,9 +24,6 @@ public class ProjectController extends CommonController {
@Autowired
private ProjectService projectService;
@Autowired
private DoneController doneController;
@Autowired
private OAuth2Provider provider;
@ -38,7 +32,7 @@ public class ProjectController extends CommonController {
@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);
@ -47,21 +41,21 @@ public class ProjectController extends CommonController {
}
@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) ? "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) ? "redirect:/done/list" : toProject(id, model);
}

View File

@ -10,14 +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.CommonController;
import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
/**
@ -28,35 +25,26 @@ import jakarta.annotation.security.RolesAllowed;
@Controller
public class NoteController extends CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@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
@ -64,20 +52,19 @@ public class NoteController extends CommonController {
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,9 +3,7 @@ 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;
@ -23,8 +21,8 @@ public class ProfileController extends CommonController {
@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

@ -44,8 +44,8 @@ body {
@media(min-width:1600px) {
.tabdivblurred {
width: 50%;
margin: auto;
width: 1111px;
}
}
@ -210,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;
}
@ -275,48 +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);
padding: 4px;
}
.emphpink {
font-weight: bolder;
color: #613583;
border: 1px solid gray;
border-radius: 8px;
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 {
@ -449,6 +472,14 @@ body {
color: white;
}
.slot_reason {
color: #26a269;
}
[data-bs-theme=dark] .slot_reason {
color: lime;
}
.flex-row-weekday {
display: flex;
flex-flow: row wrap;

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,9 @@
<font layout:fragment="title">Arbeitszeit</font>
<ul layout:fragment="menuitem">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<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>
<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">
@ -17,20 +18,19 @@
</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>
<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><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>
<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})}"
@ -39,24 +39,53 @@
</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>
<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>
<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 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>
@ -71,7 +100,9 @@
<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>
<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>
@ -80,45 +111,53 @@
</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>
<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><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 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>
<a class="btn-list" th:href="@{/done/copy/{id}(id=${done.pk})}" title="Aufgabe neu beginnen"><i class="fa fa-copy"></i></a>
<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>
<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 th:if="${daysum}">
<td>Start: <span class="emphgreen" th:text="${#temporals.format(daysum.daytimeFrom, 'HH:mm')}" th:if="${daysum.daytimeFrom}"></span></td>
<td>Ende: <span class="emphgreen" th:text="${#temporals.format(daysum.daytimeUntil, 'HH:mm')}" th:if="${daysum.daytimeUntil}"></span></td>
<td>Arbeitszeit total: <span class="emphblue" th:text="${#temporals.format(daysum.dayworktime, 'HH:mm')}" th:if="${daysum.dayworktime}"></span></td>
<td>Pausezeit total: <span class="emphorange" th:text="${#temporals.format(daysum.breaks, 'HH:mm')}" th:if="${daysum.breaks}"></span></td>
<td>Überstunden heute: <span class="emphred" th:text="${daysum.printDayOvertime()}"></span></td>
<td colspan="2">Überstunden total: <span class="emphpink" th:text="${daysum.printTotalOvertime()}" th:if="${daysum.totalOvertime}"></span></td>
<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 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>
@ -140,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>
@ -164,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>
@ -187,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>
@ -242,43 +286,81 @@
</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.
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">Montag</div>
<div class="col">Dienstag</div>
<div class="col">Mittwoch</div>
<div class="col">Donnerstag</div>
<div class="col">Freitag</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}">
<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}">
</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_right boldy" th:text="${s.printTime()}" th:if="${s.id}"></span>
<span class="slot_badge_right" th:unless="${s.id}">&nbsp;--:--&nbsp;</span>
<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];
@ -294,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

@ -1,21 +1,66 @@
<!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>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">
<div class="row" th:if="${bean}">
<div class="col-sm-3">ID</div>
<div class="col-sm-9" th:text="${bean.id}"></div>
<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" th:text="${#temporals.format(bean.day, 'EEEE, dd.MM.yyyy')}"></div>
<div class="col-sm-3">vereinbarte Arbeitszeit</div>
<div class="col-sm-9" th:text="${bean.printTime()}"></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>

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>

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>