Compare commits

..

23 Commits

Author SHA1 Message Date
Jörg Henke
c408e4d693 finetuning 2026-01-20 14:37:13 +01:00
Jörg Henke
9568e77f52 add, edit and delete apps 2026-01-20 14:00:14 +01:00
Jörg Henke
c843fa788d reordered table columns 2026-01-20 09:28:39 +01:00
Jörg Henke
62060311e9 more details 2026-01-19 19:21:00 +01:00
Jörg Henke
2cf5a44cf5 show workpackage info for list of projects 2026-01-19 15:10:48 +01:00
Jörg Henke
14c8a5faa8 missing current version in mvnrepository 2026-01-19 13:41:19 +01:00
Jörg Henke
2d97720895 excel download option 2026-01-19 13:35:41 +01:00
667837e24f src/main/resources/templates/projectmanagement/dashboard.html aktualisiert
navigation changed
2026-01-17 10:05:14 +01:00
f05a3fcbf5 src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
fixed bug in url
2026-01-17 10:01:04 +01:00
bc918f1f3c src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
link to workpackage app overview
2026-01-17 09:59:34 +01:00
957cd2762c src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
table for workpackages
2026-01-17 09:54:22 +01:00
5756f6e5f3 src/main/resources/templates/projectmanagement/app/assign.html aktualisiert 2026-01-17 09:46:12 +01:00
372f9b11eb src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
show description for workpackages
2026-01-17 09:43:45 +01:00
38729228d5 src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
fixes display of description
2026-01-17 09:34:11 +01:00
98119bff77 src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
better use two lines for name and description
2026-01-16 16:06:44 +01:00
9ce633df10 src/main/resources/templates/projectmanagement/app/assign.html aktualisiert
show description as tooltip
2026-01-16 16:00:43 +01:00
henkej
a4e32c0d0e added more info to app 2026-01-16 14:13:42 +01:00
henkej
0b7cde7ad0 assignment 2026-01-16 11:02:11 +01:00
Jörg Henke
9699032250 deployment damaged fixed 2026-01-15 18:16:43 +01:00
Jörg Henke
c5604d3ce8 preparations for app assignment 2026-01-15 17:52:36 +01:00
Jörg Henke
aaee7c9dff project management 2026-01-15 16:22:54 +01:00
Jörg Henke
f8290cdbb6 update work packages 2026-01-15 11:30:27 +01:00
Jörg Henke
cb8de9b119 first steps for a project management module 2026-01-14 18:29:19 +01:00
30 changed files with 2036 additions and 53 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,18 +24,20 @@ repositories {
}
dependencies {
implementation 'de.jottyfan:timetrackjooq:20240109'
implementation 'de.jottyfan:timetrackjooq:20260119'
implementation 'org.webjars:bootstrap:5.3.8'
implementation 'org.webjars:font-awesome:7.1.0'
implementation 'org.webjars:jquery:3.7.1'
implementation 'org.webjars:popper.js:2.11.7'
implementation 'org.webjars:datatables:2.3.5'
implementation 'org.webjars:datatables:2.3.6'
implementation 'org.webjars:jquery-ui:1.14.1'
implementation 'org.webjars.npm:fullcalendar:6.1.19'
implementation 'com.google.code.gson:gson';
implementation 'org.dhatim:fastexcel:0.19.0'
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"

View File

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

View File

@@ -1,23 +1,40 @@
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 = 20260119;
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) {
// deactivated; damages deployment...
// 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

@@ -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,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,96 @@
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.validation.BindingResult;
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.PostMapping;
import de.jottyfan.timetrack.modules.CommonController;
import de.jottyfan.timetrack.modules.projectmanagement.model.AppBean;
import jakarta.annotation.security.RolesAllowed;
/**
*
* @author jotty
*
*/
@Controller
public class AppController extends CommonController {
@Autowired
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")
@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("bean", service.getWorkpackage(pkWorkpackage));
model.addAttribute("project", service.getProjectOfWorkpackage(pkWorkpackage));
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());
model.addAttribute("linked", service.getWorkpackageApps(pkApp));
model.addAttribute("bundleMap", service.getBundleMap());
return "/projectmanagement/app/assign";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/workpackage/{pkWorkpackage}/app/{pkApp}/toggle")
public String toggleWorkpackageAppLinkage(@PathVariable("pkWorkpackage") Integer pkWorkpackage, @PathVariable("pkApp") Integer pkApp, final Model model) {
service.toggleWorkpackageAppLinkage(pkWorkpackage, pkApp);
return String.format("redirect:/projectmanagement/app/%s/assign", pkApp);
}
}

View File

@@ -0,0 +1,288 @@
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_BUNDLE;
import static de.jottyfan.timetrack.db.project.Tables.T_PROJECT;
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE;
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE_APP;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertValuesStep2;
import org.jooq.Name;
import org.jooq.Record1;
import org.jooq.Record7;
import org.jooq.Record8;
import org.jooq.SelectConditionStep;
import org.jooq.SelectHavingStep;
import org.jooq.SelectSeekStep1;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.project.tables.records.TBundleRecord;
import de.jottyfan.timetrack.db.project.tables.records.TWorkpackageAppRecord;
import de.jottyfan.timetrack.modules.projectmanagement.model.AppBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.ProjectBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageAppBean;
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) {
Name WORKPACKAGES = DSL.name("workpackages");
SelectHavingStep<Record8<Integer, Integer, String, String, String, Integer, String, 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,
DSL.arrayAgg(T_WORKPACKAGE.NAME).as(WORKPACKAGES))
.from(T_APP)
.leftJoin(T_WORKPACKAGE_APP).on(T_WORKPACKAGE_APP.FK_APP.eq(T_APP.PK_APP))
.leftJoin(T_WORKPACKAGE).on(T_WORKPACKAGE.PK_WORKPACKAGE.eq(T_WORKPACKAGE_APP.FK_WORKPACKAGE))
.where(fkWorkpackage == null ? DSL.trueCondition() : T_WORKPACKAGE_APP.FK_WORKPACKAGE.eq(fkWorkpackage))
.groupBy(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);
// @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() {
SelectSeekStep1<Record8<Integer, String, String, Integer, String, String, LocalDate, String>, String> sql = jooq
// @formatter:off
.select(T_WORKPACKAGE.PK_WORKPACKAGE,
T_WORKPACKAGE.NAME,
T_WORKPACKAGE.DESCRIPTION,
T_WORKPACKAGE.FK_PROJECT,
T_WORKPACKAGE.CONTRACT_URL,
T_WORKPACKAGE.MILESTONE_URL,
T_WORKPACKAGE.PLANNED_DUEDATE,
T_PROJECT.NAME)
.from(T_WORKPACKAGE)
.leftJoin(T_PROJECT).on(T_PROJECT.PK_PROJECT.eq(T_WORKPACKAGE.FK_PROJECT))
.orderBy(T_WORKPACKAGE.NAME);
// @formatter:off
List<WorkpackageBean> list = new ArrayList<>();
Iterator<Record8<Integer, String, String, Integer, String, String, LocalDate, String>> i = sql.fetch().iterator();
while (i.hasNext()) {
Record8<Integer, String, String, Integer, String, String, LocalDate, String> r = i.next();
WorkpackageBean bean = new WorkpackageBean();
bean.setPkWorkpackage(r.get(T_WORKPACKAGE.PK_WORKPACKAGE));
bean.setName(r.get(T_WORKPACKAGE.NAME));
bean.setDescription(r.get(T_WORKPACKAGE.DESCRIPTION));
bean.setFkProject(r.get(T_WORKPACKAGE.FK_PROJECT));
bean.setContractUrl(r.get(T_WORKPACKAGE.CONTRACT_URL));
bean.setMilestoneUrl(r.get(T_WORKPACKAGE.MILESTONE_URL));
bean.setPlannedDuedate(r.get(T_WORKPACKAGE.PLANNED_DUEDATE));
bean.setProjectName(r.get(T_PROJECT.NAME));
list.add(bean);
}
return list;
}
/**
* get all workpackage app linkages
*
* @param pkApp the ID of the app to look for
* @return the list of workpackage app linkages
*/
public List<WorkpackageAppBean> getWorkpackageApps(Integer pkApp) {
return jooq.selectFrom(T_WORKPACKAGE_APP).where(T_WORKPACKAGE_APP.FK_APP.eq(pkApp))
.fetchInto(WorkpackageAppBean.class);
}
/**
* toggle workpackage app linkage
*
* @param pkWorkpackage the ID of the workpackage
* @param pkApp the ID of the app
*/
public void toggleWorkpackageAppLinkage(Integer pkWorkpackage, Integer pkApp) {
SelectConditionStep<Record1<Integer>> sql = jooq
// @formatter:off
.select(T_WORKPACKAGE_APP.PK_WORKPACKAGE_APP)
.from(T_WORKPACKAGE_APP)
.where(T_WORKPACKAGE_APP.FK_WORKPACKAGE.eq(pkWorkpackage))
.and(T_WORKPACKAGE_APP.FK_APP.eq(pkApp));
// @formatter:on
Integer pkWorkpackageApp = sql.fetchOne(T_WORKPACKAGE_APP.PK_WORKPACKAGE_APP);
if (pkWorkpackageApp == null) {
InsertValuesStep2<TWorkpackageAppRecord, Integer, Integer> sql2 = jooq
// @formatter:off
.insertInto(T_WORKPACKAGE_APP,
T_WORKPACKAGE_APP.FK_WORKPACKAGE,
T_WORKPACKAGE_APP.FK_APP)
.values(pkWorkpackage, pkApp);
// @formatter:on
sql2.execute();
} else {
DeleteConditionStep<TWorkpackageAppRecord> sql2 = jooq
// @formatter:off
.deleteFrom(T_WORKPACKAGE_APP)
.where(T_WORKPACKAGE_APP.PK_WORKPACKAGE_APP.eq(pkWorkpackageApp));
// @formatter:on
sql2.execute();
}
}
/**
* get the bundle map; the key is the ID of the bundle
*
* @return a map; an empty map at least
*/
public Map<Integer, TBundleRecord> getBundleMap() {
Map<Integer, TBundleRecord> map = new HashMap<>();
jooq.selectFrom(T_BUNDLE).fetchInto(TBundleRecord.class).forEach(b -> map.put(b.getPkBundle(), b));
return map;
}
/**
* get the workpackage
*
* @param pkWorkpackage the ID of the workpackage
* @return the workpackage if found or null
*/
public WorkpackageBean getWorkpackage(Integer pkWorkpackage) {
return jooq
// @formatter:off
.selectFrom(T_WORKPACKAGE)
.where(T_WORKPACKAGE.PK_WORKPACKAGE.eq(pkWorkpackage))
.fetchOneInto(WorkpackageBean.class);
// @formatter:on
}
/**
* get the project of the workpackage
*
* @param pkWorkpackage the ID of the workpackage
* @return the project if found or null
*/
public ProjectBean getProjectOfWorkpackage(Integer pkWorkpackage) {
return jooq
// @formatter:off
.select(T_PROJECT.NAME,
T_PROJECT.DESCRIPTION)
.from(T_WORKPACKAGE)
.leftJoin(T_PROJECT).on(T_PROJECT.PK_PROJECT.eq(T_WORKPACKAGE.FK_PROJECT))
.where(T_WORKPACKAGE.PK_WORKPACKAGE.eq(pkWorkpackage))
.fetchOneInto(ProjectBean.class);
// @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

@@ -0,0 +1,72 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.timetrack.db.project.tables.records.TBundleRecord;
import de.jottyfan.timetrack.modules.projectmanagement.model.AppBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.ProjectBean;
import de.jottyfan.timetrack.modules.projectmanagement.model.WorkpackageAppBean;
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();
}
public List<WorkpackageAppBean> getWorkpackageApps(Integer fkApp) {
return repository.getWorkpackageApps(fkApp);
}
public void toggleWorkpackageAppLinkage(Integer pkWorkpackage, Integer pkApp) {
repository.toggleWorkpackageAppLinkage(pkWorkpackage, pkApp);
}
public Map<Integer, TBundleRecord> getBundleMap() {
return repository.getBundleMap();
}
public WorkpackageBean getWorkpackage(Integer pkWorkpackage) {
return repository.getWorkpackage(pkWorkpackage);
}
public ProjectBean getProjectOfWorkpackage(Integer 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,38 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.annotation.security.RolesAllowed;
import jakarta.servlet.http.HttpServletResponse;
/**
*
* @author jotty
*
*/
@Controller
public class DownloadController {
@Autowired
private DownloadService service;
@Autowired
private HttpServletResponse response;
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/download")
public @ResponseBody void getDownloadExcel() throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
String contentDisposition = String.format("attachment; filename=\"projectmanagement_%s.xlsx\"", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
service.generateExcel(response.getOutputStream());
}
}

View File

@@ -0,0 +1,39 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import static de.jottyfan.timetrack.db.project.Tables.V_EXCEL_APP;
import static de.jottyfan.timetrack.db.project.Tables.V_EXCEL_WORKPACKAGE;
import static de.jottyfan.timetrack.db.project.Tables.V_EXCEL_WORKPACKAGE_APP;
import java.util.List;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.project.tables.records.VExcelAppRecord;
import de.jottyfan.timetrack.db.project.tables.records.VExcelWorkpackageAppRecord;
import de.jottyfan.timetrack.db.project.tables.records.VExcelWorkpackageRecord;
/**
*
* @author jotty
*
*/
@Repository
public class DownloadRepository {
@Autowired
private DSLContext jooq;
public List<VExcelAppRecord> getAllApps() {
return jooq.selectFrom(V_EXCEL_APP).fetchInto(VExcelAppRecord.class);
}
public List<VExcelWorkpackageRecord> getAllWorkpackages() {
return jooq.selectFrom(V_EXCEL_WORKPACKAGE).fetchInto(VExcelWorkpackageRecord.class);
}
public List<VExcelWorkpackageAppRecord> getAllWorkpackageApps() {
return jooq.selectFrom(V_EXCEL_WORKPACKAGE_APP).fetchInto(VExcelWorkpackageAppRecord.class);
}
}

View File

@@ -0,0 +1,28 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.servlet.ServletOutputStream;
/**
*
* @author jotty
*
*/
@Service
public class DownloadService {
@Autowired
private DownloadRepository repository;
public void generateExcel(ServletOutputStream outputStream) throws IOException {
ExcelWriter writer = new ExcelWriter();
writer.addSheet("workpackages", repository.getAllWorkpackages());
writer.addSheet("workpackage apps", repository.getAllWorkpackageApps());
writer.addSheet("apps", repository.getAllApps());
writer.bundle(outputStream);
}
}

View File

@@ -0,0 +1,131 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dhatim.fastexcel.Workbook;
import org.dhatim.fastexcel.Worksheet;
import org.jooq.impl.TableRecordImpl;
import de.jottyfan.timetrack.db.project.tables.records.VExcelAppRecord;
import de.jottyfan.timetrack.db.project.tables.records.VExcelWorkpackageAppRecord;
import de.jottyfan.timetrack.db.project.tables.records.VExcelWorkpackageRecord;
/**
*
* @author jotty
*
*/
public class ExcelWriter {
private final Map<String, List<? extends TableRecordImpl<?>>> sheetMap;
public ExcelWriter() {
this.sheetMap = new HashMap<>();
}
public void addSheet(String name, List<? extends TableRecordImpl<?>> data) {
sheetMap.put(name, data);
}
public void bundle(OutputStream stream) throws IOException {
try (Workbook workbook = new Workbook(stream, "timetrack", "1.0")) {
sheetMap.forEach((key, sheetObjects) -> {
Worksheet worksheet = workbook.newWorksheet(key);
sheetObjects.forEach(so -> {
handleHeadLines(worksheet, so);
});
if (sheetObjects.size() > 0) {
Integer lineCounter = 1;
for (TableRecordImpl<?> sheetObject : sheetObjects) {
if (sheetObject instanceof VExcelWorkpackageRecord rec) {
lineCounter = fillFields(worksheet, rec, lineCounter);
} else if (sheetObject instanceof VExcelWorkpackageAppRecord rec) {
lineCounter = fillFields(worksheet, rec, lineCounter);
} else if (sheetObject instanceof VExcelAppRecord rec) {
lineCounter = fillFields(worksheet, rec, lineCounter);
} else {
throw new RuntimeException("unsupported TabelRecordImpl " + sheetObject.getClass().getSimpleName());
}
}
}
});
workbook.close();
}
stream.flush();
}
private void handleHeadLines(Worksheet worksheet, TableRecordImpl<?> tri) {
if (tri instanceof VExcelWorkpackageRecord) {
worksheet.value(0, 0, "project_id");
worksheet.value(0, 1, "project_name");
worksheet.value(0, 2, "project_description");
worksheet.value(0, 3, "funder_name");
worksheet.value(0, 4, "funder_description");
worksheet.value(0, 5, "workpackage_id");
worksheet.value(0, 6, "workpackage_name");
worksheet.value(0, 7, "workpackage_description");
worksheet.value(0, 8, "contract_url");
worksheet.value(0, 9, "milestone_url");
worksheet.value(0, 10, "planned_duedate");
} else if (tri instanceof VExcelWorkpackageAppRecord) {
worksheet.value(0, 0, "workpackage_app_id");
worksheet.value(0, 1, "workpackage_name");
worksheet.value(0, 2, "app_name");
worksheet.value(0, 3, "workpackage_id");
worksheet.value(0, 4, "app_id");
} else if (tri instanceof VExcelAppRecord) {
worksheet.value(0, 0, "app_id");
worksheet.value(0, 1, "replaced_by_app_with_id");
worksheet.value(0, 2, "name");
worksheet.value(0, 3, "description");
worksheet.value(0, 4, "basic_functionality");
worksheet.value(0, 5, "bundle_name");
worksheet.value(0, 6, "bundle_description");
worksheet.value(0, 7, "repository_url");
worksheet.value(0, 8, "orphan");
} else {
throw new RuntimeException("unsupported TabelRecordImpl " + tri.getClass().getSimpleName());
}
}
private Integer fillFields(Worksheet worksheet, VExcelWorkpackageRecord record, Integer lineCounter) {
worksheet.value(lineCounter, 0, record.getProjectId());
worksheet.value(lineCounter, 1, record.getProjectName());
worksheet.value(lineCounter, 2, record.getProjectDescription());
worksheet.value(lineCounter, 3, record.getFunderName());
worksheet.value(lineCounter, 4, record.getFunderDescription());
worksheet.value(lineCounter, 5, record.getWorkpackageId());
worksheet.value(lineCounter, 6, record.getWorkpackageName());
worksheet.value(lineCounter, 7, record.getWorkpackageDescription());
worksheet.value(lineCounter, 8, record.getContractUrl());
worksheet.value(lineCounter, 9, record.getMilestoneUrl());
worksheet.value(lineCounter, 10, record.getPlannedDuedate());
return lineCounter + 1;
}
private Integer fillFields(Worksheet worksheet, VExcelWorkpackageAppRecord record, Integer lineCounter) {
worksheet.value(lineCounter, 0, record.getWorkpackageAppId());
worksheet.value(lineCounter, 1, record.getWorkpackageName());
worksheet.value(lineCounter, 2, record.getAppName());
worksheet.value(lineCounter, 3, record.getWorkpackageId());
worksheet.value(lineCounter, 4, record.getAppId());
return lineCounter + 1;
}
private Integer fillFields(Worksheet worksheet, VExcelAppRecord record, Integer lineCounter) {
worksheet.value(lineCounter, 0, record.getAppId());
worksheet.value(lineCounter, 1, record.getReplacedByAppWithId());
worksheet.value(lineCounter, 2, record.getName());
worksheet.value(lineCounter, 3, record.getDescription());
worksheet.value(lineCounter, 4, record.getBasicFunctionality());
worksheet.value(lineCounter, 5, record.getBundleName());
worksheet.value(lineCounter, 6, record.getBundleDescription());
worksheet.value(lineCounter, 7, record.getRepositoryUrl());
worksheet.value(lineCounter, 8, record.getOrphan());
return lineCounter + 1;
}
}

View File

@@ -0,0 +1,104 @@
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.validation.BindingResult;
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.PostMapping;
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(true));
return "/projectmanagement/dashboard";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/project/add")
public String getProjectAddMask(final Model model) {
model.addAttribute("bean", new ProjectBean());
model.addAttribute("funders", service.getFunders());
return "/projectmanagement/project/item";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/project/{pkProject}/edit")
public String getProjectAddMask(@PathVariable("pkProject") Integer pkProject, final Model model) {
model.addAttribute("bean", service.getProject(pkProject));
model.addAttribute("funders", service.getFunders());
return "/projectmanagement/project/item";
}
@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/workpackage/{pkWorkpackage}/delete")
public String deleteWorkpackage(@PathVariable("pkWorkpackage") Integer pkWorkpackage) {
service.deleteWorkpackage(pkWorkpackage);
return "redirect:/projectmanagement";
}
@RolesAllowed("timetrack_user")
@GetMapping("/projectmanagement/{pkProject}/delete")
public String deleteProject(@PathVariable("pkProject") Integer pkProject) {
service.deleteProject(pkProject);
return "redirect:/projectmanagement";
}
@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";
}
@RolesAllowed("timetrack_user")
@PostMapping("/projectmanagement/upsert")
public String upsertWorkpackage(@ModelAttribute("bean") ProjectBean bean, BindingResult bindingResult, final Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("error", bindingResult.getAllErrors().toString());
return "/projectmanagement";
}
service.upsertProject(bean);
return "redirect:/projectmanagement";
}
@RolesAllowed("timetrack_user")
@PostMapping("/projectmanagement/project/{fkProject}/upsert")
public String updateWorkpackage(@PathVariable("fkProject") Integer fkProject,
@ModelAttribute("bean") WorkpackageBean bean, BindingResult bindingResult, final Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("error", bindingResult.getAllErrors().toString());
return "/projectmanagement/workpackage/item";
}
bean.setFkProject(fkProject);
service.upsertWorkpackage(bean);
return "redirect:/projectmanagement";
}
}

View File

@@ -0,0 +1,130 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import static de.jottyfan.timetrack.db.project.Tables.T_FUNDER;
import static de.jottyfan.timetrack.db.project.Tables.T_PROJECT;
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jooq.DSLContext;
import org.jooq.InsertValuesStep3;
import org.jooq.InsertValuesStep6;
import org.jooq.UpdateConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.project.tables.records.TFunderRecord;
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<TFunderRecord> getFunders() {
return jooq.selectFrom(T_FUNDER).fetchInto(TFunderRecord.class);
}
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);
}
public void updateWorkpackage(WorkpackageBean bean) {
UpdateConditionStep<TWorkpackageRecord> sql = jooq
// @formatter:off
.update(T_WORKPACKAGE)
.set(T_WORKPACKAGE.FK_PROJECT, bean.getFkProject())
.set(T_WORKPACKAGE.NAME, bean.getName())
.set(T_WORKPACKAGE.DESCRIPTION, bean.getDescription())
.set(T_WORKPACKAGE.MILESTONE_URL, bean.getMilestoneUrl())
.set(T_WORKPACKAGE.CONTRACT_URL, bean.getContractUrl())
.set(T_WORKPACKAGE.PLANNED_DUEDATE, bean.getPlannedDuedate())
.where(T_WORKPACKAGE.PK_WORKPACKAGE.eq(bean.getPkWorkpackage()));
// @formatter:on
sql.execute();
}
public void insertWorkpackage(WorkpackageBean bean) {
InsertValuesStep6<TWorkpackageRecord, Integer, String, String, String, String, LocalDate> sql = jooq
// @formatter:off
.insertInto(T_WORKPACKAGE,
T_WORKPACKAGE.FK_PROJECT,
T_WORKPACKAGE.NAME,
T_WORKPACKAGE.DESCRIPTION,
T_WORKPACKAGE.MILESTONE_URL,
T_WORKPACKAGE.CONTRACT_URL,
T_WORKPACKAGE.PLANNED_DUEDATE)
.values(bean.getFkProject(), bean.getName(), bean.getDescription(), bean.getMilestoneUrl(), bean.getContractUrl(), bean.getPlannedDuedate());
// @formatter:on
sql.execute();
}
public void deleteWorkpackage(Integer pkWorkpackage) {
jooq.deleteFrom(T_WORKPACKAGE).where(T_WORKPACKAGE.PK_WORKPACKAGE.eq(pkWorkpackage)).execute();
}
public void updateProject(ProjectBean bean) {
UpdateConditionStep<TProjectRecord> sql = jooq
// @formatter:off
.update(T_PROJECT)
.set(T_PROJECT.FK_FUNDER, bean.getFkFunder())
.set(T_PROJECT.NAME, bean.getName())
.set(T_PROJECT.DESCRIPTION, bean.getDescription())
.where(T_PROJECT.PK_PROJECT.eq(bean.getPkProject()));
// @formatter:on
sql.execute();
}
public void insertProject(ProjectBean bean) {
InsertValuesStep3<TProjectRecord, String, String, Integer> sql = jooq
// @formatter:off
.insertInto(T_PROJECT,
T_PROJECT.NAME,
T_PROJECT.DESCRIPTION,
T_PROJECT.FK_FUNDER)
.values(bean.getName(), bean.getDescription(), bean.getFkFunder());
// @formatter:on
sql.execute();
}
public void deleteProject(Integer pkProject) {
jooq.deleteFrom(T_PROJECT).where(T_PROJECT.PK_PROJECT.eq(pkProject)).execute();
}
public ProjectBean getProject(Integer pkProject) {
return jooq.selectFrom(T_PROJECT).where(T_PROJECT.PK_PROJECT.eq(pkProject)).fetchOneInto(ProjectBean.class);
}
}

View File

@@ -0,0 +1,94 @@
package de.jottyfan.timetrack.modules.projectmanagement;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.timetrack.db.project.tables.records.TFunderRecord;
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(boolean includeFunderName) {
List<ProjectBean> list = repository.getProjects();
if (includeFunderName) {
List<TFunderRecord> funders = repository.getFunders();
Map<Integer, String> funderMap = new HashMap<>();
funders.forEach(f -> funderMap.put(f.getPkFunder(), f.getName()));
for (ProjectBean bean : list) {
bean.setFunderName(funderMap.get(bean.getFkFunder()));
}
}
return list;
}
/**
* 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);
}
/**
* update the workpackage
*
* @param bean the bean
*/
public void upsertWorkpackage(WorkpackageBean bean) {
if (bean.getPkWorkpackage() == null) {
repository.insertWorkpackage(bean);
} else {
repository.updateWorkpackage(bean);
}
}
/**
* delete the work package
*
* @param pkWorkpackage the ID of the work package
*/
public void deleteWorkpackage(Integer pkWorkpackage) {
repository.deleteWorkpackage(pkWorkpackage);
}
public void upsertProject(ProjectBean bean) {
if (bean.getPkProject() == null) {
repository.insertProject(bean);
} else {
repository.updateProject(bean);
}
}
public void deleteProject(Integer pkProject) {
repository.deleteProject(pkProject);
}
public List<TFunderRecord> getFunders() {
return repository.getFunders();
}
public ProjectBean getProject(Integer pkProject) {
return repository.getProject(pkProject);
}
}

View File

@@ -0,0 +1,143 @@
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;
private String[] workpackages;
public String workpackagesString() {
String result = null;
if (workpackages != null) {
for (String s : workpackages) {
result = result == null ? s : String.format("%s, %s", result, s);
}
}
return result;
}
/**
* @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;
}
/**
* @return the workpackages
*/
public String[] getWorkpackages() {
return workpackages;
}
/**
* @param workpackages the workpackages to set
*/
public void setWorkpackages(String[] workpackages) {
this.workpackages = workpackages;
}
}

View File

@@ -0,0 +1,116 @@
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 Integer fkFunder;
private String funderName;
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());
bean.setFkFunder(r.getFkFunder());
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;
}
/**
* @return the funderName
*/
public String getFunderName() {
return funderName;
}
/**
* @param funderName the funderName to set
*/
public void setFunderName(String funderName) {
this.funderName = funderName;
}
/**
* @return the fkFunder
*/
public Integer getFkFunder() {
return fkFunder;
}
/**
* @param fkFunder the fkFunder to set
*/
public void setFkFunder(Integer fkFunder) {
this.fkFunder = fkFunder;
}
}

View File

@@ -0,0 +1,46 @@
package de.jottyfan.timetrack.modules.projectmanagement.model;
import java.io.Serializable;
/**
*
* @author jotty
*
*/
public class WorkpackageAppBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pkWorkpackageApp;
private Integer fkApp;
private Integer fkWorkpackage;
public static final WorkpackageAppBean of(Integer fkApp) {
WorkpackageAppBean bean = new WorkpackageAppBean();
bean.setFkApp(fkApp);
return bean;
}
public Integer getPkWorkpackageApp() {
return pkWorkpackageApp;
}
public void setPkWorkpackageApp(Integer pkWorkpackageApp) {
this.pkWorkpackageApp = pkWorkpackageApp;
}
public Integer getFkApp() {
return fkApp;
}
public void setFkApp(Integer fkApp) {
this.fkApp = fkApp;
}
public Integer getFkWorkpackage() {
return fkWorkpackage;
}
public void setFkWorkpackage(Integer fkWorkpackage) {
this.fkWorkpackage = fkWorkpackage;
}
}

View File

@@ -0,0 +1,161 @@
package de.jottyfan.timetrack.modules.projectmanagement.model;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Collectors;
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 Integer fkProject;
private String name;
private String description;
private String milestoneUrl;
private String contractUrl;
private LocalDate plannedDuedate;
private String projectName;
public static final WorkpackageBean of(TWorkpackageRecord r) {
WorkpackageBean bean = new WorkpackageBean();
bean.setPkWorkpackage(r.getPkWorkpackage());
bean.setFkProject(r.getFkProject());
bean.setName(r.getName());
bean.setDescription(r.getDescription());
bean.setMilestoneUrl(r.getMilestoneUrl());
bean.setContractUrl(r.getContractUrl());
bean.setPlannedDuedate(r.getPlannedDuedate());
return bean;
}
public static final WorkpackageBean of(Integer fkProject) {
WorkpackageBean bean = new WorkpackageBean();
bean.setFkProject(fkProject);
return bean;
}
public Boolean isIn(List<WorkpackageAppBean> linkages) {
return linkages.stream().map(WorkpackageAppBean::getFkWorkpackage).collect(Collectors.toSet()).contains(pkWorkpackage);
}
/**
* @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;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
/**
* @param description the description to set
*/
public void setDescription(String description) {
this.description = description;
}
/**
* @return the milestoneUrl
*/
public String getMilestoneUrl() {
return milestoneUrl;
}
/**
* @param milestoneUrl the milestoneUrl to set
*/
public void setMilestoneUrl(String milestoneUrl) {
this.milestoneUrl = milestoneUrl;
}
/**
* @return the contractUrl
*/
public String getContractUrl() {
return contractUrl;
}
/**
* @param contractUrl the contractUrl to set
*/
public void setContractUrl(String contractUrl) {
this.contractUrl = contractUrl;
}
/**
* @return the plannedDuedate
*/
public LocalDate getPlannedDuedate() {
return plannedDuedate;
}
/**
* @param plannedDuedate the plannedDuedate to set
*/
public void setPlannedDuedate(LocalDate plannedDuedate) {
this.plannedDuedate = plannedDuedate;
}
/**
* @return the projectName
*/
public String getProjectName() {
return projectName;
}
/**
* @param projectName the projectName to set
*/
public void setProjectName(String projectName) {
this.projectName = projectName;
}
}

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

@@ -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,9 @@ body {
.boldy {
font-weight: bolder;
}
.dashboardcard {
width: 312px;
margin: 8px;
padding: 0px;
}

View File

@@ -6,26 +6,29 @@
<title>Timetrack</title>
<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/datatables/2.3.6/css/dataTables.dataTables.min.css}" />
<link rel="stylesheet" type="text/css" th:href="@{/webjars/datatables/2.3.6/css/dataTables.bootstrap5.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.6/js/dataTables.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/datatables/2.3.6/js/dataTables.dataTables.min.js}"></script>
<script type="text/javascript" th:src="@{/webjars/datatables/2.3.6/js/dataTables.bootstrap5.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>
<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 +44,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,73 @@
<!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="btn btn-outline-secondary" th:href="@{/projectmanagement}">zur Projektübersicht</a>
</li>
</ul>
<main layout:fragment="content">
<div class="card">
<div class="card-header" th:text="${app.name}"></div>
<div class="card-body">
<div class="container">
<div class="row">
<div class="col-2">Beschreibung</div>
<div class="col-10" th:text="${app.description}"></div>
<div class="col-2">Basisfunktion</div>
<div class="col-10" th:text="${app.basicFunctionality}"></div>
<div class="col-2">URL der Entwicklung</div>
<div class="col-10">
<a th:href="${app.repositoryUrl}" target="_blank" th:text="${app.repositoryUrl}"></a>
</div>
<div class="col-2" th:if="${app.fkReplacedByApp}">Ersetzt durch andere App</div>
<div class="col-10" th:if="${app.fkReplacedByApp}">
<a th:href="@{/projectmanagement/app/{id}/assign(id=${app.fkReplacedByApp})}" th:text="${app.fkReplacedByApp}"></a>
</div>
<div class="col-2">Bundle</div>
<div class="col-10">
<th:block th:with="b=${bundleMap.get(app.fkBundle)}">
<div th:text="${b.name}"></div>
<div th:text="${b.description}"></div>
</th:block>
</div>
</div>
</div>
</div>
<div class="card-footer">
<table name="table" class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Beschreibung</th>
<th>Zuordnung</th>
</tr>
</thead>
<tbody>
<tr th:each="p : ${workpackages}">
<td><input type="checkbox" th:checked="${p.isIn(linked)}" /> <a th:href="@{/projectmanagement/workpackage/{w}/apps(w=${p.pkWorkpackage})}" th:text="${p.name}" th:title="${p.projectName}"></a></td>
<td th:text="${p.description}"></td>
<td><a th:href="@{/projectmanagement/workpackage/{w}/app/{a}/toggle(w=${p.pkWorkpackage},a=${app.pkApp})}" class="btn btn-outline-secondary">
<span th:if="${p.isIn(linked)}">rausschmeißen</span> <span th:unless="${p.isIn(linked)}">zuweisen</span>
</a></td>
</tr>
</table>
<script type="text/javascript">
$(document).ready(function() {
var localeUrl = '[[@{/js/dataTables/de.json}]]';
$("#table").DataTable({
"language" : {
"url" : localeUrl
}
});
});
</script>
</div>
</div>
</main>
</body>
</html>

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

@@ -0,0 +1,73 @@
<!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="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>
</ul>
<main layout:fragment="content">
<div class="card">
<div class="card-header" th:if="${bean}">
<span th:text="${project.name}" th:if="${project}"></span>:
<span th:text="${bean.name}"></span>
</div>
<div class="card-body" th:if="${bean}">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-2" th:if="${project}">Projektdetails</div>
<div class="col-sm-12 col-md-10" th:if="${project}" th:text="${project.description}"></div>
<div class="col-sm-12 col-md-2">Beschreibung</div>
<div class="col-sm-12 col-md-10" th:text="${bean.description}"></div>
<div class="col-sm-12 col-md-2">Milestone</div>
<div class="col-sm-12 col-md-10">
<a th:href="${bean.milestoneUrl}" target="_blank" th:text="${bean.milestoneUrl}" th:if="${bean.milestoneUrl}"></a>
</div>
<div class="col-sm-12 col-md-2">Vertrag</div>
<div class="col-sm-12 col-md-10" th:text="${bean.contractUrl}"></div>
<div class="col-sm-12 col-md-2">Zieldatum</div>
<div class="col-sm-12 col-md-10" th:text="${bean.plannedDuedate}"></div>
</div>
</div>
</div>
<div class="card-footer">
<table id="table" class="table table-striped">
<thead>
<tr>
<th th:unless="${bean}">Workpackage</th>
<th>Name</th>
<th>Beschreibung</th>
<th>URL</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="b : ${apps}">
<td th:text="${b.workpackagesString}" th:unless="${bean}"></td>
<td><a th:href="@{/projectmanagement/app/{id}(id=${b.pkApp})}" th:text="${b.name}"></a></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>
</div>
</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">Projekte</font>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
<a class="btn btn-outline-secondary" th:href="@{/projectmanagement/download}">Export</a>
<a class="btn btn-outline-secondary" th:href="@{/projectmanagement/apps}">alle Apps</a>
</li>
</ul>
<main layout:fragment="content">
<div class="container">
<div class="row">
<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>
<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 class="d-flex justify-content-between align-items-center" th:each="w : ${p.workpackages}">
<a th:href="@{/projectmanagement/workpackage/{id}/apps(id=${w.pkWorkpackage})}" th:text="${w.name}"></a>
<a th:href="@{/projectmanagement/workpackage/{id}(id=${w.pkWorkpackage})}" class="ms-auto">edit</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">
<a class="btn btn-outline-secondary" th:href="@{/projectmanagement/project/add}">Projekt anlegen</a>
</div>
<div class="card-body"></div>
<div class="card-footer"></div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,66 @@
<!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="btn btn-outline-secondary" th:href="@{/projectmanagement}">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/upsert}" method="post" th:object="${bean}">
<div class="container m-2">
<div class="row g-2">
<div class="col-2" th:if="${bean.pkProject}">ID</div>
<div class="col-10" th:if="${bean.pkProject}">
<input type="text" class="form-control" th:field="*{pkProject}" 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">Geldgeber</div>
<div class="col-10">
<select th:field="*{fkFunder}" class="form-select">
<option value="" label="--- bitte wählen ---" />
<option th:each="f : ${funders}" th:label="${f.name}" th:value="${f.pkFunder}" />
</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.pkProject}">
<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/{p}/delete(p=${bean.pkProject})}" class="btn btn-danger">Löschen</a>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@@ -0,0 +1,71 @@
<!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="btn btn-outline-secondary" th:href="@{/projectmanagement}">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/project/{p}/upsert(p=${bean.fkProject})}" method="post" th:object="${bean}">
<div class="container m-2">
<div class="row g-2">
<div class="col-2" th:if="${bean.pkWorkpackage}">ID</div>
<div class="col-10" th:if="${bean.pkWorkpackage}">
<input type="text" class="form-control" th:field="*{pkWorkpackage}" 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">Milestone-URL</div>
<div class="col-10">
<input type="text" class="form-control" th:field="*{milestoneUrl}" />
</div>
<div class="col-2">Auftragsdokument</div>
<div class="col-10">
<input type="text" class="form-control" th:field="*{contractUrl}" />
</div>
<div class="col-2">geplante Fertigstellung</div>
<div class="col-10">
<input type="text" class="form-control" th:field="*{plannedDuedate}" />
</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.pkWorkpackage}">
<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/workpackage/{p}/delete(p=${bean.pkWorkpackage})}" class="btn btn-danger">Löschen</a>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

View File

@@ -1,6 +1,5 @@
<!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}">
<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>Timetrack</title>
@@ -10,37 +9,54 @@
<ul layout:fragment="menu">
</ul>
<main layout:fragment="content">
<div class="card" style="width: 312px; margin: 24px">
<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">
<div class="container">
<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">
<div class="row">
<div class="col-8"><span class="spanlabel">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-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-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-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-4"><span class="emphred round-border border-frame" th:text="${sum.overdue}"></span></div>
<div class="col-8">
<span class="spanlabel">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-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-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-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-4">
<span class="emphred round-border border-frame" th:text="${sum.overdue}"></span>
</div>
</div>
</div>
<div class="card-footer">
<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> <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> <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> <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> <span class="billing">X</span><span
<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> <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> <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> <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> <span class="billing">X</span><span
th:text="${sum.getBillingTime(null)}" class="distfat"></span>
</div>
</div>
</div>
</main>
</body>