preparations for app assignment

This commit is contained in:
Jörg Henke
2026-01-15 17:52:36 +01:00
parent aaee7c9dff
commit c5604d3ce8
10 changed files with 363 additions and 12 deletions

View File

@@ -45,7 +45,7 @@ public class SecurityConfiguration {
// @formatter:off
.oauth2Login(o -> o.defaultSuccessUrl("/"))
.logout(o -> o.logoutSuccessHandler(new OidcClientInitiatedLogoutSuccessHandler(crr)))
.authorizeHttpRequests(o -> o.requestMatchers("/public/**", "/theme/**").permitAll().anyRequest().authenticated())
.authorizeHttpRequests(o -> o.requestMatchers("/public/**", "/theme/**", "/webjars/**").permitAll().anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.sessionManagement(o -> o.init(sec));
// @formatter:on

View File

@@ -0,0 +1,44 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import de.jottyfan.timetrack.modules.CommonController;
import jakarta.annotation.security.RolesAllowed;
/**
*
* @author jotty
*
*/
@Controller
public class AppController extends CommonController {
@Autowired
private AppService service;
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/apps")
public String getAllApps(final Model model) {
model.addAttribute("apps", service.getAppsOf(null));
return "/projectmanagement/app/list";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/workpackage/{pkWorkpackage}/apps")
public String getAppsOfWorkpackage(@PathVariable("pkWorkpackage") Integer pkWorkpackage, final Model model) {
model.addAttribute("apps", service.getAppsOf(pkWorkpackage));
return "/projectmanagement/app/list";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/app/{pkApp}/assign")
public String loadAssignmentToolForApp(@PathVariable("pkApp") Integer pkApp, final Model model) {
model.addAttribute("app", service.getApp(pkApp));
model.addAttribute("workpackages", service.getWorkpackages());
return "/projectmanagement/app/assign";
}
}

View File

@@ -0,0 +1,82 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import static de.jottyfan.timetrack.db.project.Tables.T_APP;
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE_APP;
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE;
import java.util.List;
import org.jooq.DSLContext;
import org.jooq.Record7;
import org.jooq.SelectConditionStep;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.modules.projectmanagement.model.AppBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageBean;
/**
*
* @author jotty
*/
@Repository
public class AppRepository {
@Autowired
private DSLContext jooq;
/**
* get all app beans for that workpackage
*
* @param fkWorkpackage the workpackage ID; if null, return all app beans
* @return the list of app beans; an empty list at least
*/
public List<AppBean> getAllAppBeans(Integer fkWorkpackage) {
SelectConditionStep<Record7<Integer, Integer, String, String, String, Integer, String>> sql = jooq
// @formatter:off
.select(T_APP.PK_APP,
T_APP.FK_BUNDLE,
T_APP.BASIC_FUNCTIONALITY,
T_APP.NAME,
T_APP.DESCRIPTION,
T_APP.FK_REPLACED_BY_APP,
T_APP.REPOSITORY_URL)
.from(T_APP)
.leftJoin(T_WORKPACKAGE_APP).on(T_WORKPACKAGE_APP.FK_APP.eq(T_APP.PK_APP))
.where(fkWorkpackage == null ? DSL.trueCondition() : T_WORKPACKAGE_APP.FK_WORKPACKAGE.eq(fkWorkpackage));
// @formatter:on
return sql.fetchInto(AppBean.class);
}
/**
* get the app
*
* @param pkApp the ID of the app
* @return the app bean or null
*/
public AppBean getApp(Integer pkApp) {
SelectConditionStep<Record7<Integer, Integer, String, String, String, Integer, String>> sql = jooq
// @formatter:off
.select(T_APP.PK_APP,
T_APP.FK_BUNDLE,
T_APP.BASIC_FUNCTIONALITY,
T_APP.NAME,
T_APP.DESCRIPTION,
T_APP.FK_REPLACED_BY_APP,
T_APP.REPOSITORY_URL)
.from(T_APP)
.where(T_APP.PK_APP.eq(pkApp));
// @formatter:on
return sql.fetchOneInto(AppBean.class);
}
/**
* get all work packages
*
* @return the list; an empty list at least
*/
public List<WorkpackageBean> getWorkpackages() {
return jooq.selectFrom(T_WORKPACKAGE).fetchInto(WorkpackageBean.class);
}
}

View File

@@ -0,0 +1,32 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.timetrack.modules.projectmanagement.model.AppBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageBean;
/**
*
* @author jotty
*
*/
@Service
public class AppService {
@Autowired
private AppRepository repository;
public List<AppBean> getAppsOf(Integer fkWorkpackage) {
return repository.getAllAppBeans(fkWorkpackage);
}
public AppBean getApp(Integer pkApp) {
return repository.getApp(pkApp);
}
public List<WorkpackageBean> getWorkpackages() {
return repository.getWorkpackages();
}
}

View File

@@ -0,0 +1,118 @@
package de.jottyfan.timetrack.modules.projectmanagement.model;
import java.io.Serializable;
/**
*
* @author jotty
*
*/
public class AppBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pkApp;
private Integer fkBundle;
private String basicFunctionality;
private String name;
private String description;
private Integer fkReplacedByApp;
private String repositoryUrl;
/**
* @return the pkApp
*/
public Integer getPkApp() {
return pkApp;
}
/**
* @param pkApp the pkApp to set
*/
public void setPkApp(Integer pkApp) {
this.pkApp = pkApp;
}
/**
* @return the fkBundle
*/
public Integer getFkBundle() {
return fkBundle;
}
/**
* @param fkBundle the fkBundle to set
*/
public void setFkBundle(Integer fkBundle) {
this.fkBundle = fkBundle;
}
/**
* @return the basicFunctionality
*/
public String getBasicFunctionality() {
return basicFunctionality;
}
/**
* @param basicFunctionality the basicFunctionality to set
*/
public void setBasicFunctionality(String basicFunctionality) {
this.basicFunctionality = basicFunctionality;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
/**
* @param description the description to set
*/
public void setDescription(String description) {
this.description = description;
}
/**
* @return the fkReplacedByApp
*/
public Integer getFkReplacedByApp() {
return fkReplacedByApp;
}
/**
* @param fkReplacedByApp the fkReplacedByApp to set
*/
public void setFkReplacedByApp(Integer fkReplacedByApp) {
this.fkReplacedByApp = fkReplacedByApp;
}
/**
* @return the repositoryUrl
*/
public String getRepositoryUrl() {
return repositoryUrl;
}
/**
* @param repositoryUrl the repositoryUrl to set
*/
public void setRepositoryUrl(String repositoryUrl) {
this.repositoryUrl = repositoryUrl;
}
}

View File

@@ -22,3 +22,6 @@ spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_
# application
server.port = ${server.port}
server.servlet.context-path = /timetrack
spring.mvc.contentnegotiation.favor-parameter=false
spring.mvc.contentnegotiation.media-types.css=text/css

View File

@@ -8,19 +8,18 @@
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/5.3.8/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/datatables/2.3.5/css/dataTables.dataTables.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/font-awesome/7.1.0/css/all.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/fullcalendar/6.1.10/main.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
<link rel="stylesheet" type="text/css" th:href="@{/public/dynamicstyle.css}">
<link rel="icon" type="image/png" sizes="32x32" th:href="@{/png/favicon/favicon-32x32.png}" />
<link rel="icon" type="image/png" sizes="16x16" th:href="@{/png/favicon/favicon-16x16.png}" />
<script th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
<script th:src="@{/webjars/bootstrap/5.3.8/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/webjars/datatables/2.3.5/js/dataTables.dataTables.min.js}"></script>
<script th:src="@{/webjars/fullcalendar/6.1.10/main.js}"></script>
<script th:src="@{/js/helper.js}"></script>
<script th:src="@{/js/clock.js}"></script>
<script th:src="@{/js/schedule.js}"></script>
<script type="text/javascript" th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/5.3.8/js/bootstrap.bundle.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/datatables/2.3.5/js/dataTables.dataTables.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/fullcalendar/6.1.19/index.global.min.js}"></script>
<script type="text/javascript" th:src="@{/js/helper.js}"></script>
<script type="text/javascript" th:src="@{/js/clock.js}"></script>
<script type="text/javascript" th:src="@{/js/schedule.js}"></script>
</head>
<body>

View File

@@ -0,0 +1,19 @@
<!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>Projektmanagement</title>
</head>
<body>
<font layout:fragment="title">Projekt</font>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="nav-link btn btn-secondary btn-white-text" th:href="@{/projectmanagement}">zur Projektübersicht</a>
</li>
</ul>
<main layout:fragment="content">
<div th:text="${app.name}"></div>
<div th:each="p : ${workpackages}" th:text="${p.name}"></div>
TODO: assign app to workpackage and store it
</main>
</body>
</html>

View File

@@ -0,0 +1,46 @@
<!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>Projektmanagement</title>
</head>
<body>
<font layout:fragment="title">Projekt</font>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="nav-link btn btn-secondary btn-white-text" th:href="@{/projectmanagement}">zur Projektübersicht</a>
</li>
</ul>
<main layout:fragment="content">
<div class="p-2">
<table id="table" class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Beschreibung</th>
<th>URL</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="b : ${apps}">
<td th:text="${b.name}"></td>
<td th:text="${b.description}"></td>
<td><a th:href="${b.repositoryUrl}" target="_blank">gitlab</a></td>
<td><a th:href="@{/projectmanagement/app/{id}/assign(id=${b.pkApp})}">zuordnen</a></td>
</tr>
</tbody>
</table>
<script type="text/javascript">
$(document).ready(function() {
var localeUrl = '[[@{/js/dataTables/de.json}]]';
$("#table").DataTable({
"language" : {
"url" : localeUrl
}
});
});
</script>
</div>
</main>
</body>
</html>

View File

@@ -16,19 +16,27 @@
<div class="col-sm-12 col-md-3 col-lg-2 dashboardcard" th:each="p : ${projects}">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span th:text="${p.funderName}"></span>:
<span class="fw-bold ms-auto" th:text="${p.name}"></span>
<span th:text="${p.funderName}"></span>: <span class="fw-bold ms-auto" th:text="${p.name}"></span>
<a th:href="@{/projectmanagement/project/{p}/edit(p=${p.pkProject})}" class="btn btn-link ms-auto" title="bearbeiten">edit</a>
</div>
<div class="card-body">
<div th:each="w : ${p.workpackages}">
<div class="d-flex justify-content-between align-items-center" th:each="w : ${p.workpackages}">
<a th:href="@{/projectmanagement/workpackage/{id}(id=${w.pkWorkpackage})}" th:text="${w.name}"></a>
<a th:href="@{/projectmanagement/workpackage/{id}/apps(id=${w.pkWorkpackage})}" class="ms-auto">apps</a>
</div>
<a th:href="@{/projectmanagement/project/{id}/addWorkpackage(id=${p.pkProject})}" class="btn btn-outline-secondary">Workpackage anlegen</a>
</div>
<div class="card-footer" th:text="${p.description}"></div>
</div>
</div>
<div class="col-sm-12 col-md-3 col-lg-2 dashboardcard">
<div class="card">
<div class="card-header">Alles</div>
<div class="card-body">
<a th:href="@{/projectmanagement/apps}">alle apps</a>
</div>
</div>
</div>
</div>
</div>
</main>