Compare commits

...

42 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
3535dbf237 fixes forwarding 2023-09-13 09:22:41 +02:00
4b8822e5ad theme persistence 2023-09-13 09:18:04 +02:00
094fa3f47a finetuning 2023-09-12 10:20:50 +02:00
698b2e6dd5 small theme corrections 2023-09-12 09:50:16 +02:00
c84ef5800a fixed deploy bug 2023-09-11 22:35:05 +02:00
3b8e0e4074 added dark mode 2023-09-11 22:23:45 +02:00
485bc5e8c3 spring security for keycloak roles 2023-08-04 19:16:18 +02:00
e7058a8b02 war deployment preparations 2023-07-29 16:45:07 +02:00
177a97d294 added example properties file 2023-07-28 20:28:34 +02:00
18d3efb87d conversion to jakarta 2023-07-28 20:23:31 +02:00
61 changed files with 2653 additions and 464 deletions

View File

@ -25,6 +25,11 @@
<arguments> <arguments>
</arguments> </arguments>
</buildCommand> </buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec> </buildSpec>
<natures> <natures>
<nature>org.eclipse.jdt.core.javanature</nature> <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 { plugins {
id 'org.springframework.boot' version '3.1.1' id 'org.springframework.boot' version '3.4.3'
id 'java' id 'java'
id 'war' id 'war'
} }
@ -7,7 +7,7 @@ plugins {
apply plugin: 'io.spring.dependency-management' apply plugin: 'io.spring.dependency-management'
group = 'de.jottyfan' group = 'de.jottyfan'
version = '1.2.9' version = '1.5.7'
description = """timetrack""" description = """timetrack"""
@ -23,33 +23,33 @@ repositories {
} }
dependencies { dependencies {
implementation 'org.apache.logging.log4j:log4j-api:2.20.0' implementation 'de.jottyfan:timetrackjooq:20240109'
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.20.0'
implementation 'org.webjars:bootstrap:5.2.3' implementation 'org.apache.logging.log4j:log4j-api:2.24.3'
implementation 'org.webjars:font-awesome:5.15.4' implementation 'org.apache.logging.log4j:log4j-core:2.24.3'
implementation 'org.webjars:jquery:3.6.4' implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.24.3'
implementation 'org.webjars:popper.js:2.9.3'
implementation 'org.webjars:datatables:1.13.2'
implementation 'org.webjars:jquery-ui:1.13.2'
implementation 'org.webjars:fullcalendar:5.11.3'
implementation 'com.google.code.gson:gson:2.10.1'; 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:2.1.8'
implementation 'org.webjars:jquery-ui:1.14.1'
implementation 'org.webjars:fullcalendar:6.1.9'
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-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-security'
implementation "org.springframework.boot:spring-boot-starter-oauth2-client" implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation 'org.springframework.security:spring-security-oauth2-authorization-server:1.1.1' 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-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-devtools'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'de.jottyfan:timetrackjooq:0.1.1'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.0.0' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:latest.release'
developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql'
@ -65,8 +65,9 @@ war {
"Implementation-Timestamp": new Date()) "Implementation-Timestamp": new Date())
} }
} }
baseName = project.name archiveBaseName = project.name
version = version archiveVersion = version
archiveFileName = 'timetrack.war'
} }
test { test {

20
debian/build.sh vendored
View File

@ -1,20 +0,0 @@
#!/bin/bash
mkdir -p timetrack/var/lib
cd ..
./gradlew clean build
G=$(grep "^version =" build.gradle | sed -e "s/version = //g" | sed -e "s/'//g")
echo "found version $G"
cp -v build/libs/timetrack-$G.jar debian/timetrack/var/lib/timetrack.jar
cd debian
sed -i timetrack/DEBIAN/control -e "s/Version: */Version: $G/"
V=$(grep "Version" timetrack/DEBIAN/control | sed -e "s/Version: //g")
fakeroot dpkg -b timetrack timetrack_$V.deb

View File

@ -1 +0,0 @@
/etc/timetrack.properties

View File

@ -1,9 +0,0 @@
Package: timetrack
Version:
Architecture: amd64
Maintainer: Jörg Henke <jottyfan@gmx.de>
Depends: default-jdk
Section: ship
Priority: optional
Description: timetrack application to track work times
Timetrack is a web application to track my work times for my daily reporting.

View File

@ -1,24 +0,0 @@
#!/bin/bash
mkdir -p /var/run/timetrack
groupadd timetrack || true
useradd -r -g timetrack -d /etc/timetrack -s /sbin/nologin timetrack || true
chown timetrack:timetrack -R /etc/timetrack* || true
chown timetrack:timetrack -R /var/run/timetrack || true
chown timetrack:timetrack /usr/bin/timetrack || true
cd /var/lib
systemctl daemon-reload || true
R=$(systemctl show -p SubState --value timetrack)
if [ "running" == "$R" ]
then
systemctl restart timetrack || true
systemctl reload apache2 || true
else
systemctl enable timetrack || true
echo "+------------------------------------------------------------------------------+"
echo "| configure timetrack in /etc/timetrack.properties; consider a port change... |"
echo "| start timetrack by calling sudo systemctl restart timetrack |"
echo "+------------------------------------------------------------------------------+"
fi

View File

@ -1,14 +0,0 @@
[Unit]
Description=Timetrack
After=syslog.target network.target
Before=httpd.service
[Service]
User=timetrack
Group=timetrack
PIDFile=/var/run/timetrack/timetrack.pid
ExecStart=/usr/bin/timetrack
StandardOutput=null
[Install]
WantedBy=multi-user.target

View File

@ -1,21 +0,0 @@
# jooq
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/timetrack
spring.datasource.username=timetrack
spring.datasource.password=timetrack
# application
server.port = 8083
server.servlet.context-path=/timetrack
# keycloak
keycloak.auth-server-url = http://localhost:8080/
keycloak.realm = jottyfan
keycloak.resource = timetrack
keycloak.public-client = true
keycloak.security-constraints[0].authRoles[0] = timetrack_user
keycloak.security-constraints[0].securityCollections[0].patterns[0] = /*
#keycloak.credentia
keycloak.use-resource-role-mappings=true
#keycloak.bearer-only=true

View File

@ -1,3 +0,0 @@
#!/bin/bash
java -Dspring.config.location=/etc/timetrack.properties -jar /var/lib/timetrack.jar

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

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

View File

@ -0,0 +1,24 @@
package de.jottyfan.timetrack.component;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
*
* @author jotty
*
*/
@Component
public class OAuth2Provider {
/**
* get the name of the authenticated user or null if not logged in
*
* @return the name or null
*/
public String getName() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication == null ? null : authentication.getName();
}
}

View File

@ -0,0 +1,78 @@
package de.jottyfan.timetrack.config;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
/**
*
* @author jotty
*
*/
@Configuration
public class AuthorizationConfiguration {
private static final String REALM_ACCESS_CLAIM = "realm_access";
private static final String ROLES_CLAIM = "roles";
private static final String RESOURCE_ACCESS_CLAIM = "resource_access";
@Value("${spring.security.oauth2.client.registration.keycloak.client-id}")
private String clientId;
@Bean
GrantedAuthoritiesMapper userAuthoritiesMapperForKeycloak() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
var authority = authorities.iterator().next();
boolean isOidc = authority instanceof OidcUserAuthority;
if (isOidc) {
var oidcUserAuthority = (OidcUserAuthority) authority;
var userInfo = oidcUserAuthority.getUserInfo();
if (userInfo.hasClaim(REALM_ACCESS_CLAIM)) {
var realmAccess = userInfo.getClaimAsMap(REALM_ACCESS_CLAIM);
@SuppressWarnings("unchecked")
var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
if (userInfo.hasClaim(RESOURCE_ACCESS_CLAIM)) {
var resourceAccess = userInfo.getClaimAsMap(RESOURCE_ACCESS_CLAIM);
if (resourceAccess.containsKey(clientId)) {
@SuppressWarnings("unchecked")
var roles = (Collection<String>) ((Map<?, ?>) resourceAccess.get(clientId)).get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
} else {
var oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
if (userAttributes.containsKey(REALM_ACCESS_CLAIM)) {
@SuppressWarnings("unchecked")
var realmAccess = (Map<String, Object>) userAttributes.get(REALM_ACCESS_CLAIM);
@SuppressWarnings("unchecked")
var roles = (Collection<String>) realmAccess.get(ROLES_CLAIM);
mappedAuthorities.addAll(generateAuthoritiesFromClaim(roles));
}
}
return mappedAuthorities;
};
}
private Collection<GrantedAuthority> generateAuthoritiesFromClaim(Collection<String> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
}
}

View File

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

View File

@ -1,27 +1,21 @@
package de.jottyfan.timetrack.config; package de.jottyfan.timetrack.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter; import org.springframework.security.config.Customizer;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import de.jottyfan.timetrack.config.converter.KeycloakRealmRoleConverter; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/** /**
* *
* @author henkej * @author jotty
* *
*/ */
@Configuration @Configuration
@ -29,8 +23,10 @@ import de.jottyfan.timetrack.config.converter.KeycloakRealmRoleConverter;
@EnableMethodSecurity @EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Value("${spring.security.oauth2.client.provider.keycloak.jwk-set-uri}") @Bean
private String jwtSetUri; protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity sec, InMemoryClientRegistrationRepository crr) public SecurityFilterChain securityFilterChain(HttpSecurity sec, InMemoryClientRegistrationRepository crr)
@ -39,20 +35,10 @@ public class SecurityConfiguration {
// @formatter:off // @formatter:off
.oauth2Login(o -> o.defaultSuccessUrl("/")) .oauth2Login(o -> o.defaultSuccessUrl("/"))
.logout(o -> o.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(crr))) .logout(o -> o.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(crr)))
.authorizeHttpRequests(o -> o.requestMatchers("/public/**").permitAll().anyRequest().authenticated()) .authorizeHttpRequests(o -> o.requestMatchers(AntPathRequestMatcher.antMatcher("/public/**"), AntPathRequestMatcher.antMatcher("/theme/**")).permitAll().anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(getConverter()))); .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.sessionManagement(o -> o.init(sec));
// @formatter:on // @formatter:on
return sec.build(); return sec.build();
} }
private Converter<Jwt, ? extends AbstractAuthenticationToken> getConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());
return jwtConverter;
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwtSetUri).build();
}
} }

View File

@ -1,29 +0,0 @@
package de.jottyfan.timetrack.config.converter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
/**
*
* @author henkej
*
*/
public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Object o = jwt.getClaims().get("realm_access");
@SuppressWarnings("unchecked")
final Map<String, Object> realmAccess = (Map<String, Object>) o;
Object o2 = realmAccess.get("roles");
@SuppressWarnings("unchecked")
List<String> l = (List<String>) o2;
return l.stream().map(roleName -> "ROLE_" + roleName).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
}

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,12 +12,12 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import de.jottyfan.timetrack.modules.done.DoneBean; import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.done.DoneModel;
import de.jottyfan.timetrack.modules.done.DoneService; import de.jottyfan.timetrack.modules.done.DoneService;
import de.jottyfan.timetrack.modules.done.SummaryBean; 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.annotation.security.RolesAllowed;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -28,12 +28,15 @@ import jakarta.servlet.http.HttpServletRequest;
* *
*/ */
@Controller @Controller
public class IndexController { public class IndexController extends CommonController {
private static final Logger LOGGER = LogManager.getLogger(IndexController.class); private static final Logger LOGGER = LogManager.getLogger(IndexController.class);
@Autowired @Autowired
private DoneService doneService; private DoneService doneService;
@Autowired
private OAuth2Provider provider;
@GetMapping("/logout") @GetMapping("/logout")
public String getLogout(HttpServletRequest request) throws ServletException { public String getLogout(HttpServletRequest request) throws ServletException {
request.logout(); request.logout();
@ -41,9 +44,9 @@ public class IndexController {
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping("/") @GetMapping("/")
public String getIndex(@ModelAttribute DoneModel doneModel, Model model, OAuth2AuthenticationToken token) { public String getIndex(@ModelAttribute("doneModel") DoneModel doneModel, Model model, OAuth2AuthenticationToken token) {
String username = doneService.getCurrentUser(token); String username = provider.getName();
Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file
LocalDate day = LocalDate.now(); LocalDate day = LocalDate.now();
List<DoneBean> list = doneService.getList(day, username); List<DoneBean> list = doneService.getList(day, username);

View File

@ -5,13 +5,15 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import de.jottyfan.timetrack.modules.CommonController;
/** /**
* *
* @author jotty * @author jotty
* *
*/ */
@Controller @Controller
public class CalendarController { public class CalendarController extends CommonController {
@Autowired @Autowired
private CalendarService service; private CalendarService service;

View File

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

View File

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

View File

@ -6,16 +6,25 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMethod; 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.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.annotation.security.RolesAllowed;
import jakarta.websocket.server.PathParam;
/** /**
* *
@ -23,53 +32,80 @@ import jakarta.annotation.security.RolesAllowed;
* *
*/ */
@Controller @Controller
public class DoneController { @SessionAttributes("doneModel")
public class DoneController extends CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired @Autowired
private DoneService doneService; private DoneService doneService;
@ModelAttribute("doneModel")
DoneModel getdoneModel() {
return new DoneModel();
}
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/list") @GetMapping("/done/list")
public String getList(@ModelAttribute DoneModel doneModel, Model model, OAuth2AuthenticationToken token) { public String getList(Model model, @ModelAttribute("doneModel") DoneModel doneModel) {
String username = doneService.getCurrentUser(token); String username = provider.getName();
Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file Duration maxWorkTime = Duration.ofHours(8); // TODO: to the configuration file
LocalDate day = doneModel.getDay(); LocalDate day = doneModel.getDay();
List<DoneBean> list = doneService.getList(day, username); List<DoneBean> list = doneService.getList(day, username);
List<DoneBean> week = doneService.getWeek(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); SummaryBean weekBean = new SummaryBean(week, day, maxWorkTime);
model.addAttribute("doneList", list); model.addAttribute("doneList", list);
model.addAttribute("doneModel", doneModel); Duration sumtimeDuration = Duration.ofMinutes(0);
model.addAttribute("sum", bean); 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("schedule", weekBean.toJson());
model.addAttribute("recentList", doneService.getListRecent(username, 10));
model.addAttribute("projectList", doneService.getProjects(false)); model.addAttribute("projectList", doneService.getProjects(false));
model.addAttribute("moduleList", doneService.getModules(false)); model.addAttribute("moduleList", doneService.getModules(false));
model.addAttribute("jobList", doneService.getJobs(false)); model.addAttribute("jobList", doneService.getJobs(false));
model.addAttribute("billingList", doneService.getBillings(false)); model.addAttribute("billingList", doneService.getBillings(false));
model.addAttribute("favorites", doneService.getFavorites(username));
return "done/list"; return "done/list";
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/done/abort/{day}") @PostMapping("/done/list")
public String abort(@PathVariable String day, Model model, OAuth2AuthenticationToken token) { public String getListForDate(Model model, @ModelAttribute("doneModel") DoneModel doneModel) {
DoneModel doneModel = new DoneModel(); return getList(model, doneModel);
doneModel.setDayString(day);
return getList(doneModel, model, token);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/add/{day}", method = RequestMethod.GET) @GetMapping("/done/update/{id}")
public String toAdd(@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate day, Model model) { 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(); DoneBean bean = new DoneBean();
bean.setLocalDate(day); bean.setLocalDate(day);
return toItem(bean, model); return toItem(bean, model);
} }
private String toItem(DoneBean bean, Model model) { private String toItem(DoneBean bean, Model model) {
DoneModel doneModel = new DoneModel();
doneModel.setDay(bean.getLocalDate());
model.addAttribute("doneBean", bean); model.addAttribute("doneBean", bean);
model.addAttribute("doneModel", doneModel);
model.addAttribute("projectList", doneService.getProjects(true)); model.addAttribute("projectList", doneService.getProjects(true));
model.addAttribute("moduleList", doneService.getModules(true)); model.addAttribute("moduleList", doneService.getModules(true));
model.addAttribute("jobList", doneService.getJobs(true)); model.addAttribute("jobList", doneService.getJobs(true));
@ -78,8 +114,8 @@ public class DoneController {
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/done/edit/{id}") @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); DoneBean bean = doneService.getBean(id);
if (bean == null) { if (bean == null) {
bean = new DoneBean(); // the add case; typically, only add from today bean = new DoneBean(); // the add case; typically, only add from today
@ -88,22 +124,149 @@ public class DoneController {
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert", method = RequestMethod.POST) @GetMapping("/done/end/{id}")
public String doUpsert(Model model, @ModelAttribute DoneBean bean, OAuth2AuthenticationToken token) { public String end(@PathVariable("id") Integer id, Model model) {
String username = doneService.getCurrentUser(token); 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); Integer amount = doneService.doUpsert(bean, username);
DoneModel doneModel = new DoneModel(); return amount.equals(1) ? "redirect:/done/list" : "redirect:/" + toItem(bean.getPk(), model);
doneModel.setDay(bean.getLocalDate()); }
return amount.equals(1) ? getList(doneModel, model, token) : 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") @RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/{id}") @GetMapping(value = "/done/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) { public String doDelete(@PathVariable("id") Integer id, Model model) {
DoneBean bean = doneService.getBean(id);
Integer amount = doneService.doDelete(id); Integer amount = doneService.doDelete(id);
DoneModel doneModel = new DoneModel(); return amount.equals(1) ? "redirect:/done/list" : "redirect:/" + toItem(id, model);
doneModel.setDay(bean.getLocalDate()); }
return amount.equals(1) ? getList(doneModel, model, token) : 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; 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_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_BILLING;
import static de.jottyfan.timetrack.db.done.Tables.V_JOB; import static de.jottyfan.timetrack.db.done.Tables.V_JOB;
import static de.jottyfan.timetrack.db.done.Tables.V_MODULE; import static de.jottyfan.timetrack.db.done.Tables.V_MODULE;
@ -20,22 +21,32 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext; import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep; import org.jooq.DeleteConditionStep;
import org.jooq.InsertOnDuplicateStep;
import org.jooq.InsertReturningStep;
import org.jooq.InsertValuesStep7; import org.jooq.InsertValuesStep7;
import org.jooq.Record5;
import org.jooq.Record7; import org.jooq.Record7;
import org.jooq.Record8;
import org.jooq.Result; import org.jooq.Result;
import org.jooq.SelectConditionStep; import org.jooq.SelectConditionStep;
import org.jooq.SelectLimitPercentStep;
import org.jooq.UpdateConditionStep; import org.jooq.UpdateConditionStep;
import org.jooq.exception.DataAccessException; import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository; 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.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.VBillingRecord;
import de.jottyfan.timetrack.db.done.tables.records.VJobRecord; 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.VModuleRecord;
import de.jottyfan.timetrack.db.done.tables.records.VProjectRecord; import de.jottyfan.timetrack.db.done.tables.records.VProjectRecord;
import de.jottyfan.timetrack.db.profile.tables.records.TLoginRecord; import de.jottyfan.timetrack.db.profile.tables.records.TLoginRecord;
import de.jottyfan.timetrack.help.LocalDateHelper; 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) private List<DoneBean> getAllOfInterval(LocalDateTime start, LocalDateTime end, Integer userId)
throws DataAccessException, ClassNotFoundException, SQLException { 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 // @formatter:off
.select(T_DONE.PK, .select(T_DONE.PK,
T_DONE.TIME_FROM, T_DONE.TIME_FROM,
@ -238,8 +249,14 @@ public class DoneGateway {
T_DONE.FK_PROJECT, T_DONE.FK_PROJECT,
T_DONE.FK_MODULE, T_DONE.FK_MODULE,
T_DONE.FK_JOB, T_DONE.FK_JOB,
T_DONE.FK_BILLING) T_DONE.FK_BILLING,
T_FAVORITE.PK_FAVORITE)
.from(T_DONE) .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())) .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.TIME_UNTIL.between(start, end).or(T_DONE.TIME_UNTIL.isNull()))
.and(T_DONE.FK_LOGIN.eq(userId == null ? -999999 : userId)); .and(T_DONE.FK_LOGIN.eq(userId == null ? -999999 : userId));
@ -250,6 +267,45 @@ public class DoneGateway {
Map<Integer, VModuleRecord> moduleMap = getModuleMap(); Map<Integer, VModuleRecord> moduleMap = getModuleMap();
Map<Integer, VJobRecord> jobMap = getJobMap(); Map<Integer, VJobRecord> jobMap = getJobMap();
Map<Integer, VBillingRecord> billingMap = getBillingMap(); 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()) { for (Record7<Integer, LocalDateTime, LocalDateTime, Integer, Integer, Integer, Integer> r : sql.fetch()) {
DoneBean bean = new DoneBean(); DoneBean bean = new DoneBean();
bean.setPk(r.get(T_DONE.PK)); bean.setPk(r.get(T_DONE.PK));
@ -262,7 +318,6 @@ public class DoneGateway {
bean.setBilling(billingMap.get(r.get(T_DONE.FK_BILLING))); bean.setBilling(billingMap.get(r.get(T_DONE.FK_BILLING)));
list.add(bean); list.add(bean);
} }
list.sort((o1, o2) -> o1 == null ? 0 : o1.compareTo(o2));
return list; return list;
} }
@ -397,4 +452,90 @@ public class DoneGateway {
LOGGER.debug(sql.toString()); LOGGER.debug(sql.toString());
return sql.execute(); 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; 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.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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.VJobRecord;
import de.jottyfan.timetrack.db.done.tables.records.VModuleRecord; import de.jottyfan.timetrack.db.done.tables.records.VModuleRecord;
import de.jottyfan.timetrack.db.done.tables.records.VProjectRecord; 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; import de.jottyfan.timetrack.modules.note.NoteService;
/** /**
@ -29,6 +41,13 @@ import de.jottyfan.timetrack.modules.note.NoteService;
@Transactional(transactionManager = "transactionManager") @Transactional(transactionManager = "transactionManager")
public class DoneService { public class DoneService {
private static final Logger LOGGER = LogManager.getLogger(NoteService.class); private static final Logger LOGGER = LogManager.getLogger(NoteService.class);
private static final int INTERVAL = 15;
@Autowired
private TimeService timeService;
@Autowired
private DoneRepository repository;
@Autowired @Autowired
private DSLContext dsl; private DSLContext dsl;
@ -142,4 +161,184 @@ public class DoneService {
return -1; 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

@ -1,18 +1,17 @@
package de.jottyfan.timetrack.modules.done.job; package de.jottyfan.timetrack.modules.done.job;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.done.tables.records.TJobRecord; import de.jottyfan.timetrack.db.done.tables.records.TJobRecord;
import de.jottyfan.timetrack.modules.done.DoneController; import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.done.DoneModel; import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
/** /**
@ -21,38 +20,43 @@ import jakarta.annotation.security.RolesAllowed;
* *
*/ */
@Controller @Controller
public class JobController { public class JobController extends CommonController {
@Autowired @Autowired
private JobService jobService; private JobService jobService;
@Autowired @Autowired
private DoneController doneController; private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/done/edit/job/{id}") @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); TJobRecord job = jobService.get(id);
model.addAttribute("jobBean", job); model.addAttribute("jobBean", job);
model.addAttribute("theme", profileService.getTheme(username));
return "done/job"; return "done/job";
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/job", method = RequestMethod.POST) @PostMapping("/done/upsert/job")
public String doUpsert(Model model, @ModelAttribute TJobRecord bean, OAuth2AuthenticationToken token) { public String doUpsert(Model model, @ModelAttribute("bean") TJobRecord bean) {
Integer amount = jobService.doUpsert(bean); Integer amount = jobService.doUpsert(bean);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toJob(bean.getPk(), model); return amount.equals(1) ? "redirect:/done/list": toJob(bean.getPk(), model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/add/job", method = RequestMethod.GET) @GetMapping("/done/add/job")
public String toAddJob(Model model) { public String toAddJob(Model model) {
return toJob(null, model); return toJob(null, model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/job/{id}") @GetMapping(value = "/done/delete/job/{id}")
public String doDeleteJob(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) { public String doDeleteJob(@PathVariable("id") Integer id, Model model) {
Integer amount = jobService.doDelete(id); Integer amount = jobService.doDelete(id);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : 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.io.Serializable;
import java.time.Duration; import java.time.Duration;
@ -37,9 +37,11 @@ public class DoneBean implements Serializable, Comparable<DoneBean> {
private Integer fkModule; private Integer fkModule;
private Integer fkJob; private Integer fkJob;
private Integer fkBilling; private Integer fkBilling;
private Boolean isFavorite;
public DoneBean() { public DoneBean() {
this.day = null; this.day = null;
isFavorite = false;
} }
public DoneBean(TDoneRecord r, Map<Integer, VProjectRecord> projectMap, Map<Integer, VModuleRecord> moduleMap, 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.fkModule = module.getPk();
this.fkJob = activity.getPk(); this.fkJob = activity.getPk();
this.fkBilling = billing.getPk(); this.fkBilling = billing.getPk();
isFavorite = false;
} }
private final String nullable(Object o, String format) { 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(",module=").append(module == null ? "" : module.getName());
buf.append(",activity=").append(activity == null ? "" : activity.getName()); buf.append(",activity=").append(activity == null ? "" : activity.getName());
buf.append(",billing=").append(billing == null ? "" : billing.getName()); buf.append(",billing=").append(billing == null ? "" : billing.getName());
buf.append(",isFavorite=").append(isFavorite);
buf.append("}"); buf.append("}");
return buf.toString(); return buf.toString();
} }
@ -383,4 +387,18 @@ public class DoneBean implements Serializable, Comparable<DoneBean> {
public void setFkBilling(Integer fkBilling) { public void setFkBilling(Integer fkBilling) {
this.fkBilling = 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.io.Serializable;
import java.time.LocalDate; import java.time.LocalDate;
@ -19,7 +19,7 @@ public class DoneModel implements Serializable {
private LocalDate day; private LocalDate day;
public DoneModel() { public DoneModel() {
this.day = LocalDate.now(); day = LocalDate.now();
} }
public String getDayString() { 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.io.Serializable;
import java.time.Duration; import java.time.Duration;

View File

@ -1,22 +1,18 @@
package de.jottyfan.timetrack.modules.done.module; package de.jottyfan.timetrack.modules.done.module;
import jakarta.annotation.security.RolesAllowed;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.done.tables.records.TModuleRecord; import de.jottyfan.timetrack.db.done.tables.records.TModuleRecord;
import de.jottyfan.timetrack.modules.done.DoneController; import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.done.DoneModel; import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed;
/** /**
* *
@ -24,39 +20,44 @@ import de.jottyfan.timetrack.modules.done.DoneModel;
* *
*/ */
@Controller @Controller
public class ModuleController { public class ModuleController extends CommonController {
@Autowired @Autowired
private ModuleService moduleService; private ModuleService moduleService;
@Autowired @Autowired
private DoneController doneController; private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/done/edit/module/{id}") @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); TModuleRecord module = moduleService.get(id);
model.addAttribute("moduleBean", module); model.addAttribute("moduleBean", module);
model.addAttribute("theme", profileService.getTheme(username));
return "done/module"; return "done/module";
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/module", method = RequestMethod.POST) @PostMapping("/done/upsert/module")
public String doUpsert(Model model, @ModelAttribute TModuleRecord bean, OAuth2AuthenticationToken token) { public String doUpsert(Model model, @ModelAttribute("bean") TModuleRecord bean) {
Integer amount = moduleService.doUpsert(bean); Integer amount = moduleService.doUpsert(bean);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toModule(bean.getPk(), model); return amount.equals(1) ? "redirect:/done/list" : toModule(bean.getPk(), model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/add/module", method = RequestMethod.GET) @GetMapping("/done/add/module")
public String toAddModule(Model model) { public String toAddModule(Model model) {
return toModule(null, model); return toModule(null, model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/module/{id}") @GetMapping(value = "/done/delete/module/{id}")
public String doDeleteModule(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) { public String doDeleteModule(@PathVariable("id") Integer id, Model model) {
Integer amount = moduleService.doDelete(id); Integer amount = moduleService.doDelete(id);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toModule(id, model); return amount.equals(1) ? "redirect:/done/list" : toModule(id, model);
} }
} }

View File

@ -1,18 +1,17 @@
package de.jottyfan.timetrack.modules.done.project; package de.jottyfan.timetrack.modules.done.project;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord; import de.jottyfan.timetrack.db.done.tables.records.TProjectRecord;
import de.jottyfan.timetrack.modules.done.DoneController; import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.done.DoneModel; import de.jottyfan.timetrack.modules.profile.ProfileService;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
/** /**
@ -21,38 +20,43 @@ import jakarta.annotation.security.RolesAllowed;
* *
*/ */
@Controller @Controller
public class ProjectController { public class ProjectController extends CommonController {
@Autowired @Autowired
private ProjectService projectService; private ProjectService projectService;
@Autowired @Autowired
private DoneController doneController; private OAuth2Provider provider;
@Autowired
private ProfileService profileService;
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/done/edit/project/{id}") @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); TProjectRecord project = projectService.get(id);
model.addAttribute("projectBean", project); model.addAttribute("projectBean", project);
model.addAttribute("theme", profileService.getTheme(username));
return "done/project"; return "done/project";
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/upsert/project", method = RequestMethod.POST) @PostMapping("/done/upsert/project")
public String doUpsert(Model model, @ModelAttribute TProjectRecord bean, OAuth2AuthenticationToken token) { public String doUpsert(Model model, @ModelAttribute("bean") TProjectRecord bean) {
Integer amount = projectService.doUpsert(bean); Integer amount = projectService.doUpsert(bean);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toProject(bean.getPk(), model); return amount.equals(1) ? "redirect:/done/list" : toProject(bean.getPk(), model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/done/add/project", method = RequestMethod.GET) @GetMapping("/done/add/project")
public String toAddProject(Model model) { public String toAddProject(Model model) {
return toProject(null, model); return toProject(null, model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping(value = "/done/delete/project/{id}") @GetMapping(value = "/done/delete/project/{id}")
public String doDeleteProject(@PathVariable Integer id, Model model, OAuth2AuthenticationToken token) { public String doDeleteProject(@PathVariable("id") Integer id, Model model) {
Integer amount = projectService.doDelete(id); Integer amount = projectService.doDelete(id);
return amount.equals(1) ? doneController.getList(new DoneModel(), model, token) : toProject(id, model); return amount.equals(1) ? "redirect:/done/list" : toProject(id, model);
} }
} }

View File

@ -4,16 +4,17 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.db.note.enums.EnumCategory; import de.jottyfan.timetrack.db.note.enums.EnumCategory;
import de.jottyfan.timetrack.db.note.enums.EnumNotetype; import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
import de.jottyfan.timetrack.modules.CommonController;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
/** /**
@ -22,13 +23,13 @@ import jakarta.annotation.security.RolesAllowed;
* *
*/ */
@Controller @Controller
public class NoteController { public class NoteController extends CommonController {
@Autowired @Autowired
private NoteService noteService; private NoteService noteService;
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/list") @GetMapping("/note/list")
public String getList(Model model) { public String getList(Model model) {
List<NoteBean> list = noteService.getList(); List<NoteBean> list = noteService.getList();
model.addAttribute("noteList", list); model.addAttribute("noteList", list);
@ -36,14 +37,14 @@ public class NoteController {
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/add", method = RequestMethod.GET) @GetMapping("/note/add")
public String toAdd(Model model) { public String toAdd(Model model, OAuth2AuthenticationToken token) {
return toItem(null, model); return toItem(null, model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/note/edit/{id}") @GetMapping("/note/edit/{id}")
public String toItem(@PathVariable Integer id, Model model) { public String toItem(@PathVariable("id") Integer id, Model model) {
NoteBean bean = noteService.getBean(id); NoteBean bean = noteService.getBean(id);
if (bean == null) { if (bean == null) {
bean = new NoteBean(); // the add case bean = new NoteBean(); // the add case
@ -55,15 +56,15 @@ public class NoteController {
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/upsert", method = RequestMethod.POST) @PostMapping("/note/upsert")
public String doUpsert(Model model, @ModelAttribute NoteBean bean) { public String doUpsert(Model model, @ModelAttribute("bean") NoteBean bean, OAuth2AuthenticationToken token) {
Integer amount = noteService.doUpsert(bean); Integer amount = noteService.doUpsert(bean);
return amount.equals(1) ? getList(model) : toItem(bean.getPk(), model); return amount.equals(1) ? getList(model) : toItem(bean.getPk(), model);
} }
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping(value = "/note/delete/{id}") @GetMapping(value = "/note/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model) { public String doDelete(@PathVariable("id") Integer id, Model model, OAuth2AuthenticationToken token) {
Integer amount = noteService.doDelete(id); Integer amount = noteService.doDelete(id);
return amount.equals(1) ? getList(model) : toItem(id, model); return amount.equals(1) ? getList(model) : toItem(id, model);
} }

View File

@ -0,0 +1,30 @@
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.PostMapping;
import de.jottyfan.timetrack.component.OAuth2Provider;
import de.jottyfan.timetrack.modules.CommonController;
/**
*
* @author jotty
*
*/
@Controller
public class ProfileController extends CommonController {
@Autowired
private OAuth2Provider provider;
@Autowired
private ProfileService service;
@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,58 @@
package de.jottyfan.timetrack.modules.profile;
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.InsertOnDuplicateSetMoreStep;
import org.jooq.Record1;
import org.jooq.SelectConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.profile.tables.records.TProfileRecord;
/**
*
* @author jotty
*
*/
@Repository
public class ProfileRepository {
private static final Logger LOGGER = LogManager.getLogger(ProfileRepository.class);
@Autowired
private DSLContext jooq;
public void setTheme(String username, String theme) {
InsertOnDuplicateSetMoreStep<TProfileRecord> sql = jooq
// @formatter:off
.insertInto(T_PROFILE,
T_PROFILE.USERNAME,
T_PROFILE.THEME)
.values(username, theme)
.onConflict(T_PROFILE.USERNAME)
.doUpdate()
.set(T_PROFILE.THEME, theme);
// @formatter:on
LOGGER.trace(sql.toString());
sql.execute();
}
public String getTheme(String username) {
SelectConditionStep<Record1<String>> sql = jooq
// @formatter:off
.select(T_PROFILE.THEME)
.from(T_PROFILE)
.where(T_PROFILE.USERNAME.eq(username));
// @formatter:on
LOGGER.trace(sql.toString());
Record1<String> res = sql.fetchOne();
if (res == null) {
return "light"; // default
} else {
return res.get(T_PROFILE.THEME);
}
}
}

View File

@ -0,0 +1,24 @@
package de.jottyfan.timetrack.modules.profile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* @author jotty
*
*/
@Service
public class ProfileService {
@Autowired
private ProfileRepository repository;
public void setTheme(String username, String theme) {
repository.setTheme(username, theme);
}
public String getTheme(String username) {
return username == null ? "light" : repository.getTheme(username);
}
}

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

@ -1,25 +1,24 @@
# include properties file from /etc
spring.config.import = /etc/timetrack.properties
# jooq # jooq
spring.datasource.driver-class-name = org.postgresql.Driver spring.datasource.driver-class-name = org.postgresql.Driver
# todo: export to /etc/timetrack spring.datasource.url = ${db.url}
spring.datasource.url = jdbc:postgresql://localhost:5432/timetrack spring.datasource.username = ${db.username}
spring.datasource.username = timetrack spring.datasource.password = ${db.password}
spring.datasource.password = timetrack
# security # security
keycloak.url = http://localhost:8080/realms/jottyfan spring.security.oauth2.client.registration.keycloak.client-id = ${keycloak.client-id}
keycloak.openid.url = ${keycloak.url}/protocol/openid-connect
spring.security.oauth2.client.registration.keycloak.client-id = timetrack
spring.security.oauth2.client.registration.keycloak.scope = openid spring.security.oauth2.client.registration.keycloak.scope = openid
spring.security.oauth2.client.registration.keycloak.authorization-grant-type = authorization_code spring.security.oauth2.client.registration.keycloak.authorization-grant-type = authorization_code
# todo: export to /etc/timetrack spring.security.oauth2.client.registration.keycloak.redirect-uri = ${keycloak.redirect-uri}
spring.security.oauth2.client.registration.keycloak.redirect-uri = http://localhost:8888/timetrack/login/oauth2/code/timetrack spring.security.oauth2.client.provider.keycloak.issuer-uri = ${keycloak.issuer-uri}
spring.security.oauth2.client.provider.keycloak.issuer-uri = ${keycloak.url} spring.security.oauth2.client.provider.keycloak.authorization-uri = ${keycloak.openid-url}/auth
spring.security.oauth2.client.provider.keycloak.authorization-uri = ${keycloak.openid.url}/auth spring.security.oauth2.client.provider.keycloak.token-uri = ${keycloak.openid-url}/token
spring.security.oauth2.client.provider.keycloak.token-uri = ${keycloak.openid.url}/token spring.security.oauth2.client.provider.keycloak.user-info-uri = ${keycloak.openid-url}/userinfo
spring.security.oauth2.client.provider.keycloak.user-info-uri = ${keycloak.openid.url}/userinfo spring.security.oauth2.client.provider.keycloak.jwk-set-uri = ${keycloak.openid-url}/certs
spring.security.oauth2.client.provider.keycloak.jwk-set-uri = ${keycloak.openid.url}/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username
# application # application
server:.port = 8083 server.port = ${server.port}
server.servlet.context-path = /timetrack server.servlet.context-path = /timetrack

View File

@ -7,6 +7,10 @@ body {
height: calc(100% - 56px); height: calc(100% - 56px);
} }
[data-bs-theme=dark] body {
background-color: rgb(36, 31, 49);
}
.openedSelect { .openedSelect {
overflow: auto; overflow: auto;
} }
@ -21,6 +25,10 @@ body {
color: black; color: black;
} }
[data-bs-theme=dark] .navlinkstyle {
color: #aaa;
}
.navlinkstyle:hover { .navlinkstyle:hover {
color: #1a5fb4; color: #1a5fb4;
} }
@ -29,6 +37,18 @@ body {
background-color: ghostwhite; background-color: ghostwhite;
} }
[data-bs-theme=dark] .navback {
color: ghostwhite;
background-color: #222;
}
@media(min-width:1600px) {
.tabdivblurred {
margin: auto;
width: 1111px;
}
}
.tabdivblurred { .tabdivblurred {
padding: 8px; padding: 8px;
padding-bottom: 0px; padding-bottom: 0px;
@ -43,6 +63,10 @@ body {
background-image: linear-gradient(to bottom, #ffc, #ee0) !important; background-image: linear-gradient(to bottom, #ffc, #ee0) !important;
} }
[data-bs-theme=dark] .noty {
background-image: linear-gradient(to bottom, rgb(0, 0, 0), rgb(229, 165, 10)) !important;
}
.glassy { .glassy {
background-color: rgba(0, 0, 0s, 0.1); background-color: rgba(0, 0, 0s, 0.1);
} }
@ -56,6 +80,10 @@ body {
min-width: 95vw !important; min-width: 95vw !important;
} }
[data-bs-theme=dark] .formpane {
background: #111;
}
.menudangerbutton { .menudangerbutton {
color: #e00 !important; color: #e00 !important;
border: 1px solid rgba(0, 0, 0, 0); border: 1px solid rgba(0, 0, 0, 0);
@ -86,6 +114,11 @@ body {
!important; !important;
} }
[data-bs-theme=dark] .page {
background-image: linear-gradient(to bottom, #123, #111)
!important;
}
.emph { .emph {
border-radius: 3px !important; border-radius: 3px !important;
border: 1px solid #3070b0 !important; border: 1px solid #3070b0 !important;
@ -94,6 +127,13 @@ body {
!important; !important;
} }
[data-bs-theme=dark] .emph {
color: #ffffff !important;
background-image: linear-gradient(to bottom, #113531 0%, #135 100%)
!important;
}
.doneoverviewtext { .doneoverviewtext {
font-size: 120%; font-size: 120%;
} }
@ -131,22 +171,54 @@ body {
font-size: smaller font-size: smaller
} }
[data-bs-theme=dark] .billing {
color: white !important;
}
.WP2 { .WP2 {
background: radial-gradient(#ffff00, #ffe169) !important; background: radial-gradient(#ffff00, #ffe169) !important;
} }
[data-bs-theme="dark"] .WP2 {
color: black !important;
}
.WP4 { .WP4 {
color: black;
background: radial-gradient(#00ffff, #69c3ff) !important; background: radial-gradient(#00ffff, #69c3ff) !important;
} }
[data-bs-theme="dark"] .WP4 {
color: black !important;
}
.WP5 { .WP5 {
color: black;
background: radial-gradient(#ff0000, #e396ff) !important; background: radial-gradient(#ff0000, #e396ff) !important;
} }
[data-bs-theme="dark"] .WP5 {
color: black !important;
}
.TA3 { .TA3 {
color: black;
background: radial-gradient(#99ff99, #ccffcc) !important; background: radial-gradient(#99ff99, #ccffcc) !important;
} }
[data-bs-theme="dark"] .TA3 {
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 { .left {
text-align: left; text-align: left;
} }
@ -170,13 +242,17 @@ body {
.version { .version {
font-size: small; font-size: small;
color: silver; color: black;
position: absolute; position: absolute;
padding-top: 36px; padding-top: 36px;
padding-left: 22px; padding-left: 22px;
z-index: 0; z-index: 0;
} }
[data-bs-theme="dark"] .version {
color: white;
}
.fc-content { .fc-content {
cursor: pointer; cursor: pointer;
} }
@ -194,6 +270,10 @@ body {
border-radius: 4px; border-radius: 4px;
} }
[data-bs-theme=dark] .hoverlink {
color: white;
}
.hoverlink:hover { .hoverlink:hover {
color: white; color: white;
background-image: linear-gradient(to right bottom, #99c1f1, #1a5f64); background-image: linear-gradient(to right bottom, #99c1f1, #1a5f64);
@ -204,39 +284,62 @@ body {
} }
.emphgreen { .emphgreen {
font-weight: bolder;
color: #136600; color: #136600;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white); background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
} }
.emphblue { .emphblue {
font-weight: bolder;
color: #1a5fb4; color: #1a5fb4;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white); background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
} }
.emphorange { .emphorange {
font-weight: bolder;
color: #c64600; color: #c64600;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white); background-image: linear-gradient(to left, #e6e6e6, white);
padding: 4px;
} }
.emphred { .emphred {
font-weight: bolder;
color: #a51d2d; color: #a51d2d;
border: 1px solid gray;
border-radius: 8px;
background-image: linear-gradient(to left, #e6e6e6, white); 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; 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 { .tab-pane-table {
@ -246,6 +349,10 @@ body {
border: 1px solid silver; border: 1px solid silver;
} }
[data-bs-theme=dark] .tab-pane-table {
background-color: rgb(36, 31, 49);
}
.tab-pane-glassy { .tab-pane-glassy {
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
padding: 8px; padding: 8px;
@ -294,3 +401,95 @@ body {
max-height: calc(90vh - 100px); max-height: calc(90vh - 100px);
margin: 8px; 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

@ -4,29 +4,30 @@
* Coolock Definition * Coolock Definition
*/ */
class Clock { class Clock {
constructor(size, selector, color, background) { constructor(size, selector, arrowcolor, scalecolor, backgroundcolor) {
var canvas = $("<canvas>"); var canvas = $("<canvas>");
canvas.attr("id", "clockPanel"); canvas.attr("id", "clockPanel");
canvas.attr("width", size); canvas.attr("width", size);
canvas.attr("height", size); canvas.attr("height", size);
canvas.css("border-radius", "4px"); canvas.css("border-radius", "4px");
canvas.css("background", background); canvas.css("background", backgroundcolor);
canvas[0].getContext("2d").canvas.width = size; canvas[0].getContext("2d").canvas.width = size;
canvas[0].getContext("2d").canvas.height = size - 3; // -3 removes unneeded overlap canvas[0].getContext("2d").canvas.height = size;
$(selector).append(canvas); $(selector).append(canvas);
var ctx = canvas[0].getContext("2d"); var ctx = canvas[0].getContext("2d");
this.ctx = ctx; this.ctx = ctx;
this.size = size; this.size = size;
this.background = background; this.background = backgroundcolor;
this.color = color; this.color = arrowcolor;
this.scalecolor = scalecolor;
this.redraw(); this.redraw();
} }
drawCircle = function(ctx, x, y, r, w) { drawCircle = function(ctx, x, y, r, w) {
ctx.strokeStyle = this.color; ctx.strokeStyle = this.background;
ctx.lineWidth = w; ctx.lineWidth = w;
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.arc(x, y, r, 0, 2 * Math.PI);
@ -35,7 +36,7 @@ class Clock {
drawArrow = function(ctx, x1, y1, x2, y2, col) { drawArrow = function(ctx, x1, y1, x2, y2, col) {
ctx.strokeStyle = col; ctx.strokeStyle = col;
ctx.fillStyle = this.color; ctx.fillStyle = this.background;
ctx.lineCap = "round"; ctx.lineCap = "round";
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.beginPath(); ctx.beginPath();

View File

@ -1,3 +1,16 @@
toggleTheme = function(urlprefix) {
var oldValue = $("html").attr("data-bs-theme");
var newValue = oldValue == "dark" ? "light" : "dark";
$("html").attr("data-bs-theme", newValue);
var url = urlprefix + "/profile/" + newValue;
$.ajax({
url: url,
dataType: 'json',
type: "POST",
contentType: 'application/json'
});
}
resetValue = function(selector, value) { resetValue = function(selector, value) {
$(selector).val(value); $(selector).val(value);
$(selector).change(); $(selector).change();
@ -21,10 +34,10 @@ validateTime = function(inputField, okButtonId) {
var regexPattern = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/; var regexPattern = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
var valid = value == "" ? true : regexPattern.test(value); var valid = value == "" ? true : regexPattern.test(value);
if (valid) { if (valid) {
$(inputField).css("background-color", "#cfc"); $(inputField).css("background-color", "#cfc").css("color", "black");
$("[id='" + okButtonId + "']").removeAttr('disabled'); $("[id='" + okButtonId + "']").removeAttr('disabled');
} else { } else {
$(inputField).css("background-color", "#fcc"); $(inputField).css("background-color", "#fcc").css("color", "black");
$("[id='" + okButtonId + "']").attr('disabled', 'disabled'); $("[id='" + okButtonId + "']").attr('disabled', 'disabled');
} }
} }

View File

@ -53,11 +53,15 @@ class Schedule {
} }
time2pixel = function (time, hourHeight) { time2pixel = function (time, hourHeight) {
var timeArray = time.split(":"); if (time == null) {
var hours = parseInt(timeArray[0]); return 0;
var minutes = parseInt(timeArray[1]); } else {
var pixels = parseInt((hours + (minutes / 60)) * hourHeight); var timeArray = time.split(":");
return pixels; 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) { drawSlot = function(ctx, slotNr, from, until, color, fillColor) {

View File

@ -10,7 +10,7 @@
<main layout:fragment="content"> <main layout:fragment="content">
<div class="container formpane"> <div class="container formpane">
<form th:action="@{/contact/upsert}" th:object="${contactBean}" method="post"> <form th:action="@{/contact/upsert}" th:object="${contactBean}" method="post">
<div class="row mb-3"> <div class="row mb-3" th:style="${contactBean.pk == null ? 'display: none' : ''}">
<label for="inputPk" class="col-sm-2 col-form-label">Inhalt von Eintrag</label> <label for="inputPk" class="col-sm-2 col-form-label">Inhalt von Eintrag</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input id="inputPk" type="text" th:field="*{pk}" class="form-control" readonly="readonly" /> <input id="inputPk" type="text" th:field="*{pk}" class="form-control" readonly="readonly" />

View File

@ -20,7 +20,7 @@
<div class="accordion-body"> <div class="accordion-body">
<div class="row row-cols-12 ro-cols-lg-4 ro-cols-md-3 ro-cols-sd-2 g-4" style="margin: 8px"> <div class="row row-cols-12 ro-cols-lg-4 ro-cols-md-3 ro-cols-sd-2 g-4" style="margin: 8px">
<div class="col" th:each="contact : ${contactList}"> <div class="col" th:each="contact : ${contactList}">
<div class="card text-dark bg-light shadow" style="width: 18rem"> <div class="card shadow" style="width: 18rem">
<div class="card-header text-center"> <div class="card-header text-center">
<font th:text="${contact.forename} + ' ' + ${contact.surname}" style="font-size: larger"></font> <font th:text="${contact.forename} + ' ' + ${contact.surname}" style="font-size: larger"></font>
</div> </div>
@ -38,7 +38,7 @@
</div> </div>
</div> </div>
<div id="div_list" class="tab-pane fade tab-pane-table"> <div id="div_list" class="tab-pane fade tab-pane-table">
<div class="accordion-body" style="background-color: white"> <div class="accordion-body">
<table id="table" class="table table-striped table-condensed"> <table id="table" class="table table-striped table-condensed">
<thead> <thead>
<tr> <tr>

View File

@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" <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}">
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" layout:decorate="~{layout/main.html}">
<head> <head>
<title>Arbeitszeit</title> <title>Arbeitszeit</title>
</head> </head>
@ -8,7 +7,10 @@
<font layout:fragment="title">Arbeitszeit</font> <font layout:fragment="title">Arbeitszeit</font>
<ul layout:fragment="menuitem"> <ul layout:fragment="menuitem">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')"> <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="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"> <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}" /> <input type="date" class="form-control" th:value="*{day}" th:field="*{day}" />
@ -16,30 +18,74 @@
</div> </div>
</div> </div>
</form> </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> </li>
</ul> </ul>
<ul layout:fragment="menu"> <ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')"> <li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<table> <table>
<tr> <tr>
<td><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/{day}(day=${doneModel.day})}">Neuer <td><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/{day}(day=${doneModel.day})}">Neuer Eintrag</a></td>
Eintrag</a></td> <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> <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> </tr>
</table> </table>
</li> </li>
</ul> </ul>
<main layout:fragment="content"> <main layout:fragment="content">
<ul class="nav nav-tabs navback" role="tablist"> <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">
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_schedule">Kalender</a></li> <a class="nav-link navlinkstyle active" data-bs-toggle="tab" href="#div_list">Liste</a>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_project">Projekt</a></li> </li>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_module">Modul</a></li> <li class="nav-item">
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_job">Aufgabe</a></li> <a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_schedule">Kalender</a>
<li class="nav-item"><a class="nav-link navlinkstyle" data-bs-toggle="tab" href="#div_billing">Abrechnung</a></li> </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> </ul>
<div class="tabdivblurred tab-content"> <div class="tabdivblurred tab-content">
<div id="div_list" class="tab-pane active tab-pane-table" style="background-color: white"> <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"> <table class="table table-striped table-condensed">
<thead> <thead>
<tr> <tr>
@ -49,44 +95,70 @@
<th>Modul</th> <th>Modul</th>
<th>Aufgabe</th> <th>Aufgabe</th>
<th>Abrechnung</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> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="done : ${doneList}"> <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})}">
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span th:text="${done.timeDiff}"></span></a></td> <span th:text="${done.timeNote}"></span>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span class="boldtext" </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">
th:text="${done.project?.name}"></span></a></td> <i class="fa fa-clock"></i>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span class="boldtext" </a></td>
th:text="${done.module?.name}"></span></a></td> <td><span th:text="${done.timeDiff}"></span></td>
<td><a class="hoverlink" th:href="@{/done/edit/{id}(id=${done.pk})}"><span class="boldtext" <td>
th:text="${done.activity?.name}"></span></a></td> <select onchange="submitDropdown(this)" th:data-id="${done.pk}" data-field="project">
<td><span th:text="${done.billing.shortcut}" th:class="'billing ' + ${done.billing.csskey}" <option value="">---</option>
th:if="${done.billing != null}"></span></td> <option th:each="p : ${projectList}" th:value="${p.pk}" th:text="${p.name}" th:selected="${done.project?.name == p.name ? 'selected' : 'false'}"></option>
<td><a th:href="@{/done/edit/{id}(id=${done.pk})}" th:title="${done.pk}"><i class="fa fa-edit"></i></a></td> </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> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr th:if="${daysum}">
<td>Zusammenfassung</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>
<td>Start: <span class="emphgreen" th:text="${sum.start}"></span></td> <span class="sumfield">Ende: <span class="emphgreen round-border-right" th:text="${#temporals.format(daysum.daytimeUntil, 'HH:mm')}" th:if="${daysum.daytimeUntil}"></span></span>
<td>Ende: <span class="emphgreen" th:text="${sum.end}"></span></td> <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>
<td>Arbeitszeit total: <span class="emphblue" th:text="${sum.total}"></span></td> <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>
<td>Pausezeit total: <span class="emphorange" th:text="${sum.pause}"></span></td> <span class="sumfield">Überstunden heute: <span class="emphred round-border-right" th:text="${daysum.printDayOvertime()}"></span></span>
<td>Überstunden: <span class="emphred" th:text="${sum.overdue}"></span></td> <span class="sumfield">Überstunden total: <span class="emphpink round-border-right" th:text="${daysum.printTotalOvertime()}"></span></span>
</td>
</tr> </tr>
<tr> <tr>
<td></td> <td></td>
<td><span th:if="${sum.getBillingTime('WP2') != '0,0 h'}"><span class="billing WP2">WP2</span><span <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>
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('WP4') != '0,0 h'}"><span class="billing WP4">WP4</span><span <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>
th:text="${sum.getBillingTime('WP4')}" 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('WP5') != '0,0 h'}"><span class="billing WP5">WP5</span><span <td colspan="2"><span class="billing">X</span><span th:text="${sum.getBillingTime(null)}" class="distfat"></span></td>
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>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@ -107,13 +179,14 @@
<tr th:each="project : ${projectList}"> <tr th:each="project : ${projectList}">
<td><span th:text="${project.name}"></span></td> <td><span th:text="${project.name}"></span></td>
<td><span th:text="${project.percentUsage}"></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> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<td colspan="3"><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/project}">neues Projekt</a> <td colspan="3"><a class="nav-link btn btn-success btn-white-text" th:href="@{/done/add/project}">neues Projekt</a></td>
</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
@ -131,7 +204,9 @@
<tr th:each="module : ${moduleList}"> <tr th:each="module : ${moduleList}">
<td><span th:text="${module.name}"></span></td> <td><span th:text="${module.name}"></span></td>
<td><span th:text="${module.percentUsage}"></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> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
@ -154,7 +229,9 @@
<tr th:each="job : ${jobList}"> <tr th:each="job : ${jobList}">
<td><span th:text="${job.name}"></span></td> <td><span th:text="${job.name}"></span></td>
<td><span th:text="${job.percentUsage}"></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> </tr>
</tbody> </tbody>
<tfoot> <tfoot>
@ -182,53 +259,149 @@
</tbody> </tbody>
</table> </table>
</div> </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> </div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document)
let width = parseInt($("#schedule").css("min-width")); .ready(
let height = parseInt($("#schedule").css("min-height")); function() {
var schedule = new Schedule("#schedule", width, height); // the tab deeplink functionality
var ctx = $("#scheduleCanvas")[0].getContext("2d"); let url = location.href.replace(/\/$/, "");
var currentDayRecords = JSON.parse('[(${schedule})]'); if (location.hash) {
var scheduleRecords = currentDayRecords.schedule; const hash = url.split("#");
for (var i = 0; i < scheduleRecords.length; i++) { $('#worktimetabs a[href="#' + hash[1] + '"]').tab("show");
var r = scheduleRecords[i]; url = location.href.replace(/\/#/, "#");
var cssClass = r.billing; history.replaceState(null, null, url);
var color = "#ffffff"; }
if (cssClass == "WP5") {
color = "#aa0000"; // the schedule
} else if (cssClass == "WP4") { let width = parseInt($("#schedule").css(
color = "#0000aa"; "min-width"));
} else if (cssClass == "WP2") { let height = parseInt($("#schedule").css(
color = "#aaaa00"; "min-height"));
} else if (cssClass == "TA3") { var schedule = new Schedule("#schedule", width,
color = "#00aa00"; height);
} var ctx = $("#scheduleCanvas")[0]
/* daySlot 7 = sunday, but this should be slot 0 */ .getContext("2d");
schedule.drawSlot(ctx, r.daySlot > 6 ? 0 : r.daySlot, r.from, r.until, "black", color); var currentDayRecords = JSON
} .parse('[(${schedule})]');
var localeUrl = '[[@{/js/dataTables/de.json}]]'; var scheduleRecords = currentDayRecords.schedule;
$("#project_table").DataTable({ for (var i = 0; i < scheduleRecords.length; i++) {
"language" : { var r = scheduleRecords[i];
"url" : localeUrl var cssClass = r.billing;
} var color = "#ffffff";
}); if (cssClass == "WP5") {
$("#module_table").DataTable({ color = "#aa0000";
"language" : { } else if (cssClass == "WP4") {
"url" : localeUrl color = "#0000aa";
} } else if (cssClass == "WP2") {
}); color = "#aaaa00";
$("#job_table").DataTable({ } else if (cssClass == "TA3") {
"language" : { color = "#00aa00";
"url" : localeUrl }
} /* daySlot 7 = sunday, but this should be slot 0 */
}); schedule.drawSlot(ctx, r.daySlot > 6 ? 0
$("#billing_table").DataTable({ : r.daySlot, r.from, r.until,
"language" : { "black", color);
"url" : localeUrl }
} var localeUrl = '[[@{/js/dataTables/de.json}]]';
}); $("#project_table").DataTable({
}); "language" : {
"url" : localeUrl
}
});
$("#module_table").DataTable({
"language" : {
"url" : localeUrl
}
});
$("#job_table").DataTable({
"language" : {
"url" : localeUrl
}
});
$("#billing_table").DataTable({
"language" : {
"url" : localeUrl
}
});
});
</script> </script>
</main> </main>
</body> </body>

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

@ -1,30 +1,30 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> <html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" data-bs-theme="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Timetrack</title> <title>Timetrack</title>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/5.2.3/css/bootstrap.min.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/1.13.2/css/dataTables.bootstrap5.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/5.15.4/css/all.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/5.11.3/main.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="@{/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="32x32" th:href="@{/png/favicon/favicon-32x32.png}" />
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/png/favicon/favicon-16x16.png}" /> <link rel="icon" type="image/png" sizes="16x16" th:href="@{/png/favicon/favicon-16x16.png}" />
<script th:src="@{/webjars/jquery/3.6.4/jquery.min.js}"></script> <script th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
<script th:src="@{/webjars/bootstrap/5.2.3/js/bootstrap.bundle.min.js}"></script> <script th:src="@{/webjars/bootstrap/5.3.3/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/webjars/datatables/1.13.2/js/jquery.dataTables.min.js}"></script> <script th:src="@{/webjars/datatables/2.1.8/js/dataTables.dataTables.min.js}"></script>
<script th:src="@{/webjars/datatables/1.13.2/js/dataTables.bootstrap5.min.js}"></script> <script th:src="@{/webjars/fullcalendar/6.1.9/main.js}"></script>
<script th:src="@{/webjars/fullcalendar/5.11.3/main.js}"></script>
<script th:src="@{/js/helper.js}"></script> <script th:src="@{/js/helper.js}"></script>
<script th:src="@{/js/clock.js}"></script> <script th:src="@{/js/clock.js}"></script>
<script th:src="@{/js/schedule.js}"></script> <script th:src="@{/js/schedule.js}"></script>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-light bg-light static-top"> <nav class="navbar navbar-expand-lg static-top">
<div class="container-fluid" style="width: 98%"> <div class="container-fluid" style="width: 98%">
<i class="fa fa-calendar-alt"></i> <a class="navbar-brand" style="margin-left: 8px; z-index: 1" th:href="@{/}">Timetrack</a><br /> <i class="fa fa-calendar-alt"></i> <a class="navbar-brand" style="margin-left: 8px; z-index: 1" th:href="@{/}">Timetrack</a><br />
<div class="version" th:text="${@manifestBean.getVersion()}"></div> <div class="version" th:text="${@manifestBean.getVersion()}"></div>
@ -47,12 +47,36 @@
<li class="nav-item"><a class="nav-link titlemod"><font layout:fragment="title"></font></a></li> <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="menuitem" style="list-style-type: none"></li>
<li layout:fragment="menu" 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"></div></li> <li class="nav-item ms-auto"><div id="clock" class="clock" th:attr="onclick=|toggleTheme('${baseUrl}');resetClock()|"></div></li>
</ul> </ul>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
new Clock(38, "#clock", "#000", "rgba(255, 255, 255, 0.0)"); //<![CDATA[
$(document).ready(function(){
var theme = "[[${theme}]]";
$("html").attr("data-bs-theme", theme);
resetClock = function() {
$("#clock").empty();
var theme = $("html").attr("data-bs-theme");
var arrowcolor;
var scalecolor;
var backgroundcolor;
if (theme == "dark") {
arrowcolor = "#fff";
scalecolor = "#aaa";
backgroundcolor = "rgba(0, 0, 0, 0.3)"
} else {
arrowcolor = "#099";
scalecolor = "#000";
backgroundcolor = "rgba(0, 0, 0, 0)";
}
new Clock(38, "#clock", arrowcolor, scalecolor, backgroundcolor);
}
resetClock();
});
//]]>
</script> </script>
</nav> </nav>
<main layout:fragment="content" class="page body"></main> <main layout:fragment="content" class="page body"></main>

View File

@ -10,7 +10,7 @@
<main layout:fragment="content"> <main layout:fragment="content">
<div class="container formpane"> <div class="container formpane">
<form th:action="@{/note/upsert}" th:object="${noteBean}" method="post"> <form th:action="@{/note/upsert}" th:object="${noteBean}" method="post">
<div class="row mb-3"> <div class="row mb-3" th:style="${noteBean.pk == null ? 'display: none' : ''}">
<label for="inputPk" class="col-sm-2 col-form-label">Inhalt von Eintrag</label> <label for="inputPk" class="col-sm-2 col-form-label">Inhalt von Eintrag</label>
<div class="col-sm-10"> <div class="col-sm-10">
<input id="inputPk" type="text" th:field="*{pk}" class="form-control" readonly="readonly" /> <input id="inputPk" type="text" th:field="*{pk}" class="form-control" readonly="readonly" />

View File

@ -19,7 +19,7 @@
<div id="div_dashboard" class="tab-pane active"> <div id="div_dashboard" class="tab-pane active">
<div class="row row-cols-12 ro-cols-lg-3 ro-cols-md-2 ro-cols-sd-1 g-4" style="margin: 8px"> <div class="row row-cols-12 ro-cols-lg-3 ro-cols-md-2 ro-cols-sd-1 g-4" style="margin: 8px">
<div class="col" th:each="note : ${noteList}"> <div class="col" th:each="note : ${noteList}">
<div class="card text-dark bg-light shadow" style="width: 100%"> <div class="card shadow" style="width: 100%">
<div class="card-header text-center"> <div class="card-header text-center">
<font th:text="${note.category}" style="font-size: larger">:</font> <font th:text="${note.title}" <font th:text="${note.category}" style="font-size: larger">:</font> <font th:text="${note.title}"
style="font-size: larger; font-weight: bolder"></font> style="font-size: larger; font-weight: bolder"></font>
@ -40,7 +40,7 @@
</div> </div>
</div> </div>
<div id="div_list" class="tab-pane fade tab-pane-table"> <div id="div_list" class="tab-pane fade tab-pane-table">
<div class="accordion-body" style="background-color: white"> <div class="accordion-body">
<table id="table" class="table table-striped table-condensed"> <table id="table" class="table table-striped table-condensed">
<thead> <thead>
<tr> <tr>

View File

@ -10,22 +10,22 @@
<ul layout:fragment="menu"> <ul layout:fragment="menu">
</ul> </ul>
<main layout:fragment="content"> <main layout:fragment="content">
<div class="card text-dark bg-light" style="width: 312px; margin: 24px"> <div class="card" style="width: 312px; margin: 24px">
<div class="card-header"><a class="btn btn-seondary btn-bordered btn-secondaryhover" style="width: 100%" <div class="card-header"><a class="btn btn-seondary btn-bordered btn-secondaryhover" style="width: 100%"
th:href="@{/done/list}">heutige Arbeitszeiten</a></div> th:href="@{/done/list}">heutige Arbeitszeiten</a></div>
<div class="card-body"> <div class="card-body">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-8"><span class="spanlabel">Start:</span></div> <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-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-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-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-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> </div>
</div> </div>

View File

@ -0,0 +1,10 @@
# example configuration for the /etc/timetrack.properties file
db.url = jdbc:postgresql://localhost:5432/timetrack
db.username = timetrack
db.password = timetrack
keycloak.client-id = timetrack
keycloak.issuer-uri = http://localhost:8080/realms/jottyfan
keycloak.openid-url = http://localhost:8080/realms/jottyfan/protocol/openid-connect
keycloak.redirect-uri = http://localhost:8083/timetrack/login/oauth2/code/timetrack

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")));
}
}