Compare commits
42 Commits
f07f8f3c06
...
master
Author | SHA1 | Date | |
---|---|---|---|
d95b3a1600 | |||
2ef8a48488 | |||
5dcc64ac74 | |||
23bab9a2b4 | |||
1d532e322c | |||
c757bb5916 | |||
c4615765a5 | |||
5b296d39e9 | |||
a4bcc00363 | |||
4820232b31 | |||
f8f501f1b2 | |||
a52793de46 | |||
e38f62fa72 | |||
1f71d9edeb | |||
b779590309 | |||
689a601c8c | |||
5117fd0e71 | |||
c1b8283dd0 | |||
7fc30ffe48 | |||
568dfc8a64 | |||
8be05b8afc | |||
742446e46e | |||
48168aaf65 | |||
9373eacab7 | |||
8b51b595d6 | |||
d702d6816b | |||
f11723505e | |||
a737adf8c1 | |||
4f5db460ae | |||
ee41117a57 | |||
e7d9d74269 | |||
0cc5cdb945 | |||
3535dbf237 | |||
4b8822e5ad | |||
094fa3f47a | |||
698b2e6dd5 | |||
c84ef5800a | |||
3b8e0e4074 | |||
485bc5e8c3 | |||
e7058a8b02 | |||
177a97d294 | |||
18d3efb87d |
5
.project
5
.project
@ -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>
|
||||||
|
2
.settings/org.springframework.ide.eclipse.prefs
Normal file
2
.settings/org.springframework.ide.eclipse.prefs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
boot.validation.initialized=true
|
||||||
|
eclipse.preferences.version=1
|
39
build.gradle
39
build.gradle
@ -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
20
debian/build.sh
vendored
@ -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
|
|
1
debian/timetrack/DEBIAN/conffiles
vendored
1
debian/timetrack/DEBIAN/conffiles
vendored
@ -1 +0,0 @@
|
|||||||
/etc/timetrack.properties
|
|
9
debian/timetrack/DEBIAN/control
vendored
9
debian/timetrack/DEBIAN/control
vendored
@ -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.
|
|
24
debian/timetrack/DEBIAN/postinst
vendored
24
debian/timetrack/DEBIAN/postinst
vendored
@ -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
|
|
@ -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
|
|
21
debian/timetrack/etc/timetrack.properties
vendored
21
debian/timetrack/etc/timetrack.properties
vendored
@ -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
|
|
3
debian/timetrack/usr/bin/timetrack
vendored
3
debian/timetrack/usr/bin/timetrack
vendored
@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
java -Dspring.config.location=/etc/timetrack.properties -jar /var/lib/timetrack.jar
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
@ -79,7 +115,7 @@ 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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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() {
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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:/";
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,12 +53,16 @@ class Schedule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
time2pixel = function (time, hourHeight) {
|
time2pixel = function (time, hourHeight) {
|
||||||
|
if (time == null) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
var timeArray = time.split(":");
|
var timeArray = time.split(":");
|
||||||
var hours = parseInt(timeArray[0]);
|
var hours = parseInt(timeArray[0]);
|
||||||
var minutes = parseInt(timeArray[1]);
|
var minutes = parseInt(timeArray[1]);
|
||||||
var pixels = parseInt((hours + (minutes / 60)) * hourHeight);
|
var pixels = parseInt((hours + (minutes / 60)) * hourHeight);
|
||||||
return pixels;
|
return pixels;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drawSlot = function(ctx, slotNr, from, until, color, fillColor) {
|
drawSlot = function(ctx, slotNr, from, until, color, fillColor) {
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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,14 +259,108 @@
|
|||||||
</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}"> --:-- </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2"><a th:href="@{/done/slot/back}" class="btn btn-outline-primary"><- 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 -></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) {
|
||||||
|
const hash = url.split("#");
|
||||||
|
$('#worktimetabs a[href="#' + hash[1] + '"]').tab("show");
|
||||||
|
url = location.href.replace(/\/#/, "#");
|
||||||
|
history.replaceState(null, null, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the schedule
|
||||||
|
let width = parseInt($("#schedule").css(
|
||||||
|
"min-width"));
|
||||||
|
let height = parseInt($("#schedule").css(
|
||||||
|
"min-height"));
|
||||||
|
var schedule = new Schedule("#schedule", width,
|
||||||
|
height);
|
||||||
|
var ctx = $("#scheduleCanvas")[0]
|
||||||
|
.getContext("2d");
|
||||||
|
var currentDayRecords = JSON
|
||||||
|
.parse('[(${schedule})]');
|
||||||
var scheduleRecords = currentDayRecords.schedule;
|
var scheduleRecords = currentDayRecords.schedule;
|
||||||
for (var i = 0; i < scheduleRecords.length; i++) {
|
for (var i = 0; i < scheduleRecords.length; i++) {
|
||||||
var r = scheduleRecords[i];
|
var r = scheduleRecords[i];
|
||||||
@ -205,7 +376,9 @@
|
|||||||
color = "#00aa00";
|
color = "#00aa00";
|
||||||
}
|
}
|
||||||
/* daySlot 7 = sunday, but this should be slot 0 */
|
/* daySlot 7 = sunday, but this should be slot 0 */
|
||||||
schedule.drawSlot(ctx, r.daySlot > 6 ? 0 : r.daySlot, r.from, r.until, "black", color);
|
schedule.drawSlot(ctx, r.daySlot > 6 ? 0
|
||||||
|
: r.daySlot, r.from, r.until,
|
||||||
|
"black", color);
|
||||||
}
|
}
|
||||||
var localeUrl = '[[@{/js/dataTables/de.json}]]';
|
var localeUrl = '[[@{/js/dataTables/de.json}]]';
|
||||||
$("#project_table").DataTable({
|
$("#project_table").DataTable({
|
||||||
|
68
src/main/resources/templates/done/slot/item.html
Normal file
68
src/main/resources/templates/done/slot/item.html
Normal 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>
|
59
src/main/resources/templates/done/slot/range.html
Normal file
59
src/main/resources/templates/done/slot/range.html
Normal 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>
|
@ -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>
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
10
src/main/resources/timetrack.properties
Normal file
10
src/main/resources/timetrack.properties
Normal 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
|
@ -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")));
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user