add, edit and delete apps

This commit is contained in:
Jörg Henke
2026-01-20 14:00:14 +01:00
parent c843fa788d
commit 9568e77f52
6 changed files with 202 additions and 3 deletions

View File

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

View File

@@ -3,10 +3,14 @@ package de.jottyfan.timetrack.modules.projectmanagement;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
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.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.timetrack.modules.CommonController; import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.projectmanagement.model.AppBean;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
/** /**
@@ -20,6 +24,43 @@ public class AppController extends CommonController {
@Autowired @Autowired
private AppService service; private AppService service;
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/app/add")
public String getAppForAdd(final Model model) {
model.addAttribute("bean", new AppBean());
model.addAttribute("apps", service.getApps());
model.addAttribute("bundles", service.getBundleMap().values());
return "/projectmanagement/app/item";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/app/{pkApp}")
public String getAppForEdit(@PathVariable("pkApp") Integer pkApp, final Model model) {
model.addAttribute("bean", service.getApp(pkApp));
model.addAttribute("apps", service.getApps());
model.addAttribute("bundles", service.getBundleMap().values());
return "/projectmanagement/app/item";
}
@RolesAllowed("timetrack_user")
@PostMapping("/projectmanagement/app/upsert")
public String updateApp(@ModelAttribute("bean") AppBean bean, BindingResult bindingResult, final Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("apps", service.getApps());
model.addAttribute("bundles", service.getBundleMap().values());
return "/projectmanagement/app/item";
}
service.upsert(bean);
return "redirect:/projectmanagement/apps";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/app/{pkApp}/delete")
public String deleteApp(@PathVariable("pkApp") Integer pkApp) {
service.deleteApp(pkApp);
return "redirect:/projectmanagement/apps";
}
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/apps") @GetMapping("/projectmanagement/apps")
public String getAllApps(final Model model) { public String getAllApps(final Model model) {

View File

@@ -192,4 +192,64 @@ public class AppRepository {
.fetchOneInto(ProjectBean.class); .fetchOneInto(ProjectBean.class);
// @formatter:on // @formatter:on
} }
/**
* get all apps ordered by name
*
* @return the apps list; an empty list at least
*/
public List<AppBean> getAllApps() {
return jooq.selectFrom(T_APP).orderBy(T_APP.NAME).fetchInto(AppBean.class);
}
/**
* delete the app
*
* @param pkApp the ID of the app
*/
public void deleteApp(Integer pkApp) {
jooq.deleteFrom(T_APP).where(T_APP.PK_APP.eq(pkApp)).execute();
}
/**
* update the app bean
*
* @param bean the bean
*/
public void updateApp(AppBean bean) {
jooq
// @formatter:off
.update(T_APP)
.set(T_APP.NAME, bean.getName())
.set(T_APP.DESCRIPTION, bean.getDescription())
.set(T_APP.FK_BUNDLE, bean.getFkBundle())
.set(T_APP.BASIC_FUNCTIONALITY, bean.getBasicFunctionality())
.set(T_APP.FK_REPLACED_BY_APP, bean.getFkReplacedByApp())
.set(T_APP.REPOSITORY_URL, bean.getRepositoryUrl())
.where(T_APP.PK_APP.eq(bean.getPkApp()))
.execute();
// @formatter:on
}
/**
* insert the bean
*
* @param bean the bean
* @return the new ID of the bean
*/
public Integer insertApp(AppBean bean) {
return jooq
// @formatter:off
.insertInto(T_APP,
T_APP.NAME,
T_APP.DESCRIPTION,
T_APP.FK_BUNDLE,
T_APP.BASIC_FUNCTIONALITY,
T_APP.FK_REPLACED_BY_APP,
T_APP.REPOSITORY_URL)
.values(bean.getName(), bean.getDescription(), bean.getFkBundle(), bean.getBasicFunctionality(), bean.getFkReplacedByApp(), bean.getRepositoryUrl())
.returning(T_APP.PK_APP)
.fetchOne(T_APP.PK_APP);
// @formatter:on
}
} }

View File

@@ -53,4 +53,20 @@ public class AppService {
public ProjectBean getProjectOfWorkpackage(Integer pkWorkpackage) { public ProjectBean getProjectOfWorkpackage(Integer pkWorkpackage) {
return repository.getProjectOfWorkpackage(pkWorkpackage); return repository.getProjectOfWorkpackage(pkWorkpackage);
} }
public List<AppBean> getApps() {
return repository.getAllApps();
}
public void deleteApp(Integer pkApp) {
repository.deleteApp(pkApp);
}
public void upsert(AppBean bean) {
if (bean.getPkApp() != null) {
repository.updateApp(bean);
} else {
repository.insertApp(bean);
}
}
} }

View File

@@ -0,0 +1,81 @@
<!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">App</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/apps}">abbrechen</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container">
<div class="alert alert-danger" th:if="${error}" th:text="${error}"></div>
<form th:action="@{/projectmanagement/app/upsert}" method="post" th:object="${bean}">
<div class="container m-2">
<div class="row g-2">
<div class="col-2" th:if="${bean.pkApp}">ID</div>
<div class="col-10" th:if="${bean.pkApp}">
<input type="text" class="form-control" th:field="*{pkApp}" readonly="readonly" />
</div>
<div class="col-2">Name</div>
<div class="col-10">
<input type="text" class="form-control" th:field="*{name}" />
</div>
<div class="col-2">Beschreibung</div>
<div class="col-10">
<textarea class="form-control" th:field="*{description}"></textarea>
</div>
<div class="col-2">Git-URL</div>
<div class="col-10">
<input type="text" class="form-control" th:field="*{repositoryUrl}" />
</div>
<div class="col-2">Basisfunktion</div>
<div class="col-10">
<textarea class="form-control" th:field="*{basicFunctionality}"></textarea>
</div>
<div class="col-2">Bundle</div>
<div class="col-10">
<select th:field="*{fkBundle}" class="form-select">
<option value="" label="--- bitte wählen ---" />
<option th:each="b : ${bundles}" th:label="${b.name}" th:value="${b.pkBundle}" />
</select>
</div>
<div class="col-2">ersetzt durch</div>
<div class="col-10">
<select th:field="*{fkReplacedByApp}" class="form-select">
<option value="" label="--- bitte wählen ---" />
<option th:each="a : ${apps}" th:value="${a.pkApp}" th:title="${a.repositoryUrl}" th:label="${a.name}" />
</select>
</div>
<div class="col-2"></div>
<div class="col-5">
<input type="submit" class="btn btn-primary" value="Speichern">
</div>
<div class="col-5" th:if="${bean.pkApp}">
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">Löschen</button>
</div>
</div>
</div>
</form>
</div>
<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">
<h5 class="modal-title" id="deleteModalLabel">Bestätigung für Löschvorgang</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">Bist du sicher, dass du dieses Element löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<a th:href="@{/projectmanagement/app/{p}/delete(p=${bean.pkApp})}" class="btn btn-danger">Löschen</a>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@@ -7,7 +7,8 @@
<font layout:fragment="title">Projekt</font> <font layout:fragment="title">Projekt</font>
<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')">
<a class="nav-link btn btn-secondary btn-white-text" th:href="@{/projectmanagement}">zur Projektübersicht</a> <a class="btn btn-outline-secondary" th:href="@{/projectmanagement}">zur Projektübersicht</a>
<a class="btn btn-outline-primary" th:href="@{/projectmanagement/app/add}" th:unless="${bean}">neue App anlegen</a>
</li> </li>
</ul> </ul>
<main layout:fragment="content"> <main layout:fragment="content">
@@ -48,7 +49,7 @@
<tbody> <tbody>
<tr th:each="b : ${apps}"> <tr th:each="b : ${apps}">
<td th:text="${b.workpackagesString}" th:unless="${bean}"></td> <td th:text="${b.workpackagesString}" th:unless="${bean}"></td>
<td th:text="${b.name}"></td> <td><a th:href="@{/projectmanagement/app/{id}(id=${b.pkApp})}" th:text="${b.name}"></a></td>
<td th:text="${b.description}"></td> <td th:text="${b.description}"></td>
<td><a th:href="${b.repositoryUrl}" target="_blank">gitlab</a></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> <td><a th:href="@{/projectmanagement/app/{id}/assign(id=${b.pkApp})}">zuordnen</a></td>