first steps for a project management module

This commit is contained in:
Jörg Henke
2026-01-14 18:29:19 +01:00
parent 71f22ca16e
commit cb8de9b119
15 changed files with 461 additions and 13 deletions

View File

@@ -7,7 +7,7 @@ plugins {
apply plugin: 'io.spring.dependency-management'
group = 'de.jottyfan'
version = '1.6.0'
version = '26.1'
description = """timetrack"""
@@ -24,7 +24,7 @@ repositories {
}
dependencies {
implementation 'de.jottyfan:timetrackjooq:20240109'
implementation 'de.jottyfan:timetrackjooq:20260114'
implementation 'org.webjars:bootstrap:5.3.8'
implementation 'org.webjars:font-awesome:7.1.0'

View File

@@ -1,23 +1,39 @@
package de.jottyfan.timetrack;
import static de.jottyfan.timetrack.db.public_.Tables.V_VERSION;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.exception.DataAccessException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
public class Main extends SpringBootServletInitializer {
private static final Integer requiredDbVersion = 20260114;
public static final Logger LOGGER = LogManager.getLogger(Main.class);
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder application) {
return application.sources(Main.class);
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
ConfigurableApplicationContext context = builder.run();
DSLContext jooq = context.getBean(DSLContext.class);
Integer foundDbVersion = null;
try {
foundDbVersion = jooq.selectFrom(V_VERSION).fetchOne(V_VERSION.VERSION);
} catch (DataAccessException e) {
String msg = String.format("Wrong database version found; %d is required, but found %d", requiredDbVersion,
foundDbVersion);
throw new RuntimeException(msg);
}
return builder.sources(Main.class);
}
public static void main(String[] args) {

View File

@@ -0,0 +1,27 @@
package de.jottyfan.timetrack.help;
import static de.jottyfan.timetrack.db.public_.Tables.V_VERSION;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
/**
*
* @author jotty
*
*/
@Repository
public class MainfestRepository {
@Autowired
private DSLContext jooq;
/**
* get the db version with title
*
* @return the DB version, prefixed by DBVersion
*/
public String getDbVersion() {
return String.format("DBVersion %s", jooq.selectFrom(V_VERSION).fetchOne(V_VERSION.VERSION));
}
}

View File

@@ -19,6 +19,13 @@ public class ManifestBean {
@Autowired(required = false)
private BuildProperties buildProperties;
@Autowired
private MainfestRepository repository;
public String getDbVersion() {
return repository.getDbVersion();
}
public String getVersion() {
StringBuilder buf = new StringBuilder();
buf.append("Version ");

View File

@@ -0,0 +1,52 @@
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 de.jottyfan.timetrack.modules.projectmanagement.model.ProjectBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageBean;
import jakarta.annotation.security.RolesAllowed;
/**
*
* @author jotty
*
*/
@Controller
public class ProjectmanagementController extends CommonController {
@Autowired
private ProjectmanagementService service;
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement")
public String getDashboard(final Model model) {
model.addAttribute("projects", service.getProjects());
return "/projectmanagement/dashboard";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/project/add")
public String getProjectAddMask(final Model model) {
model.addAttribute("bean", new ProjectBean());
return "/projectmanagement/project/add";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/workpackage/{pkWorkpackage}")
public String getWorkpackage(@PathVariable("pkWorkpackage") Integer pkWorkpackage, final Model model) {
model.addAttribute("bean", service.getWorkpackage(pkWorkpackage));
return "/projectmanagement/workpackage/item";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/project/{fkProject}/addWorkpackage")
public String getWorkpackageAddMask(@PathVariable("fkProject") Integer fkProject, final Model model) {
model.addAttribute("bean", WorkpackageBean.of(fkProject));
return "/projectmanagement/workpackage/item";
}
}

View File

@@ -0,0 +1,54 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import static de.jottyfan.timetrack.db.project.Tables.T_PROJECT;
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.project.tables.records.TProjectRecord;
import de.jottyfan.timetrack.db.project.tables.records.TWorkpackageRecord;
import de.jottyfan.timetrack.modules.projectmanagement.model.ProjectBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageBean;
/**
*
* @author jotty
*
*/
@Repository
public class ProjectmanagementRepository {
@Autowired
private DSLContext jooq;
public List<ProjectBean> getProjects() {
List<TProjectRecord> projects = jooq.selectFrom(T_PROJECT).fetchInto(TProjectRecord.class);
List<TWorkpackageRecord> workpackages = jooq.selectFrom(T_WORKPACKAGE).fetchInto(TWorkpackageRecord.class);
Map<Integer, List<WorkpackageBean>> wpMap = new HashMap<>();
workpackages.forEach(w -> {
List<WorkpackageBean> found = wpMap.get(w.getFkProject());
if (found == null) {
found = new ArrayList<>();
wpMap.put(w.getFkProject(), found);
}
found.add(WorkpackageBean.of(w));
});
List<ProjectBean> list = new ArrayList<>();
for (TProjectRecord r : projects) {
list.add(ProjectBean.of(r, wpMap.get(r.getPkProject())));
}
return list;
}
public WorkpackageBean getWorkpackage(Integer pkWorkpackage) {
return jooq.selectFrom(T_WORKPACKAGE).where(T_WORKPACKAGE.PK_WORKPACKAGE.eq(pkWorkpackage)).fetchOneInto(WorkpackageBean.class);
}
}

View File

@@ -0,0 +1,40 @@
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.ProjectBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageBean;
/**
*
* @author jotty
*
*/
@Service
public class ProjectmanagementService {
@Autowired
private ProjectmanagementRepository repository;
/**
* get all projects
*
* @return all projects
*/
public List<ProjectBean> getProjects() {
return repository.getProjects();
}
/**
* get the workpackage
*
* @param pkWorkpackage the ID of the workpackage
* @return the bean or null if not found
*/
public WorkpackageBean getWorkpackage(Integer pkWorkpackage) {
return repository.getWorkpackage(pkWorkpackage);
}
}

View File

@@ -0,0 +1,85 @@
package de.jottyfan.timetrack.modules.projectmanagement.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import de.jottyfan.timetrack.db.project.tables.records.TProjectRecord;
/**
*
* @author jotty
*
*/
public class ProjectBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pkProject;
private String name;
private String description;
private final List<WorkpackageBean> workpackages;
public ProjectBean() {
workpackages = new ArrayList<>();
}
public static final ProjectBean of(TProjectRecord r, List<WorkpackageBean> workpackages) {
ProjectBean bean = new ProjectBean();
bean.setPkProject(r.getPkProject());
bean.setName(r.getName());
bean.setDescription(r.getDescription());
if (workpackages != null) {
bean.getWorkpackages().addAll(workpackages);
}
return bean;
}
/**
* @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 workpackages
*/
public List<WorkpackageBean> getWorkpackages() {
return workpackages;
}
/**
* @return the pkProject
*/
public Integer getPkProject() {
return pkProject;
}
/**
* @param pkProject the pkProject to set
*/
public void setPkProject(Integer pkProject) {
this.pkProject = pkProject;
}
}

View File

@@ -0,0 +1,74 @@
package de.jottyfan.timetrack.modules.projectmanagement.model;
import java.io.Serializable;
import de.jottyfan.timetrack.db.project.tables.records.TWorkpackageRecord;
/**
*
* @author jotty
*
*/
public class WorkpackageBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pkWorkpackage;
private String name;
private Integer fkProject;
public static final WorkpackageBean of(TWorkpackageRecord r) {
WorkpackageBean bean = new WorkpackageBean();
bean.setPkWorkpackage(r.getPkWorkpackage());
bean.setName(r.getName());
bean.setFkProject(r.getFkProject());
return bean;
}
public static final WorkpackageBean of(Integer fkProject) {
WorkpackageBean bean = new WorkpackageBean();
bean.setFkProject(fkProject);
return bean;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the pkWorkpackage
*/
public Integer getPkWorkpackage() {
return pkWorkpackage;
}
/**
* @param pkWorkpackage the pkWorkpackage to set
*/
public void setPkWorkpackage(Integer pkWorkpackage) {
this.pkWorkpackage = pkWorkpackage;
}
/**
* @return the fkProject
*/
public Integer getFkProject() {
return fkProject;
}
/**
* @param fkProject the fkProject to set
*/
public void setFkProject(Integer fkProject) {
this.fkProject = fkProject;
}
}

View File

@@ -242,15 +242,20 @@ body {
.version {
font-size: small;
color: black;
color: gray;
position: absolute;
padding-top: 36px;
padding-left: 22px;
z-index: 0;
}
[data-bs-theme="dark"] .version {
color: white;
.dbversion {
font-size: small;
color: gray;
position: absolute;
top: 2px;
padding-left: 22px;
z-index: 0;
}
.fc-content {
@@ -494,3 +499,8 @@ body {
.boldy {
font-weight: bolder;
}
.dashboardcard {
width: 312px !important;
margin: 24px !important;
}

View File

@@ -26,6 +26,7 @@
<body>
<nav class="navbar navbar-expand-lg static-top">
<div class="container-fluid" style="width: 98%">
<div class="dbversion" th:text="${@manifestBean.getDbVersion()}"></div>
<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>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
@@ -41,6 +42,7 @@
<li><a class="dropdown-item" th:href="@{/contact/list}">Kontakte</a></li>
<li><a class="dropdown-item" th:href="@{/note/list}">Notizen</a></li>
<li><a class="dropdown-item" th:href="@{/calendar}">Kalender</a></li>
<li><a class="dropdown-item" th:href="@{/projectmanagement}">Projekte</a></li>
<li><hr /></li>
<li><a class="dropdown-item" th:href="@{/logout}">[[${currentUser}]] abmelden</a></li>
</ul></li>

View File

@@ -0,0 +1,30 @@
<!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">Projekte</font>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="nav-link btn btn-success btn-white-text" th:href="@{/projectmanagement/project/add}">Neues Projekt anlegen</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-4 col-lg-2 card dashboardcard" th:each="p : ${projects}">
<div class="card-header" th:text="${p.name}"></div>
<div class="card-body">
<div th:each="w : ${p.workpackages}">
<a th:href="@{/projectmanagement/workpackage/{id}(id=${w.pkWorkpackage})}" th:text="${w.name}"></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>
</main>
</body>
</html>

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-success btn-white-text" th:href="@{/projectmanagement}">abbrechen</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container">
TODO: Maske zum Anlegen eines neuen Projektes
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<!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">Workpackage</font>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="nav-link btn btn-success btn-white-text" th:href="@{/projectmanagement}">abbrechen</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container">
<div th:unless="${bean.pkWorkpackage}">
TODO: Maske zum Anlegen eines neuen Workpackages für Projekt <span th:text="${bean.fkProject}"></span>
</div>
<div th:if="${bean.pkWorkpackage}">
<div class="container m-2">
<div class="row g-2">
<div class="col-2">ID</div>
<div class="col-10"><span class="form-control" th:text="${bean.pkWorkpackage}"></span></div>
<div class="col-2">Name</div>
<div class="col-10"><span class="form-control" th:text="${bean.name}"></span></div>
</div>
</div>
TODO: Maske zum Bearbeiten eines bestehenden Workpackages
</div>
</div>
</main>
</body>
</html>

View File

@@ -10,7 +10,7 @@
<ul layout:fragment="menu">
</ul>
<main layout:fragment="content">
<div class="card" style="width: 312px; margin: 24px">
<div class="card dashboardcard">
<div class="card-header"><a class="btn btn-seondary btn-bordered btn-secondaryhover" style="width: 100%"
th:href="@{/done/list}">heutige Arbeitszeiten</a></div>
<div class="card-body">