Compare commits
19 Commits
c5604d3ce8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c408e4d693 | ||
|
|
9568e77f52 | ||
|
|
c843fa788d | ||
|
|
62060311e9 | ||
|
|
2cf5a44cf5 | ||
|
|
14c8a5faa8 | ||
|
|
2d97720895 | ||
| 667837e24f | |||
| f05a3fcbf5 | |||
| bc918f1f3c | |||
| 957cd2762c | |||
| 5756f6e5f3 | |||
| 372f9b11eb | |||
| 38729228d5 | |||
| 98119bff77 | |||
| 9ce633df10 | |||
|
|
a4e32c0d0e | ||
|
|
0b7cde7ad0 | ||
|
|
9699032250 |
@@ -24,18 +24,20 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'de.jottyfan:timetrackjooq:20260114'
|
||||
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"
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -17,22 +17,23 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
@EnableTransactionManagement
|
||||
public class Main extends SpringBootServletInitializer {
|
||||
|
||||
private static final Integer requiredDbVersion = 20260114;
|
||||
private static final Integer requiredDbVersion = 20260119;
|
||||
|
||||
public static final Logger LOGGER = LogManager.getLogger(Main.class);
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -20,6 +24,43 @@ 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) {
|
||||
@@ -30,6 +71,8 @@ public class AppController extends CommonController {
|
||||
@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";
|
||||
}
|
||||
@@ -39,6 +82,15 @@ public class AppController extends CommonController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
package de.jottyfan.timetrack.modules.projectmanagement;
|
||||
|
||||
import static de.jottyfan.timetrack.db.project.Tables.T_APP;
|
||||
import static de.jottyfan.timetrack.db.project.Tables.T_WORKPACKAGE_APP;
|
||||
import static de.jottyfan.timetrack.db.project.Tables.T_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;
|
||||
|
||||
/**
|
||||
@@ -33,7 +51,8 @@ public class AppRepository {
|
||||
* @return the list of app beans; an empty list at least
|
||||
*/
|
||||
public List<AppBean> getAllAppBeans(Integer fkWorkpackage) {
|
||||
SelectConditionStep<Record7<Integer, Integer, String, String, String, Integer, String>> sql = jooq
|
||||
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,
|
||||
@@ -41,10 +60,19 @@ public class AppRepository {
|
||||
T_APP.NAME,
|
||||
T_APP.DESCRIPTION,
|
||||
T_APP.FK_REPLACED_BY_APP,
|
||||
T_APP.REPOSITORY_URL)
|
||||
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))
|
||||
.where(fkWorkpackage == null ? DSL.trueCondition() : T_WORKPACKAGE_APP.FK_WORKPACKAGE.eq(fkWorkpackage));
|
||||
.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);
|
||||
}
|
||||
@@ -77,6 +105,184 @@ public class AppRepository {
|
||||
* @return the list; an empty list at least
|
||||
*/
|
||||
public List<WorkpackageBean> getWorkpackages() {
|
||||
return jooq.selectFrom(T_WORKPACKAGE).fetchInto(WorkpackageBean.class);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -29,4 +33,40 @@ public class AppService {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,17 @@ public class AppBean implements Serializable {
|
||||
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
|
||||
@@ -115,4 +126,18 @@ public class AppBean implements Serializable {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ 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;
|
||||
|
||||
@@ -21,6 +23,8 @@ public class WorkpackageBean implements Serializable {
|
||||
private String contractUrl;
|
||||
private LocalDate plannedDuedate;
|
||||
|
||||
private String projectName;
|
||||
|
||||
public static final WorkpackageBean of(TWorkpackageRecord r) {
|
||||
WorkpackageBean bean = new WorkpackageBean();
|
||||
bean.setPkWorkpackage(r.getPkWorkpackage());
|
||||
@@ -39,6 +43,10 @@ public class WorkpackageBean implements Serializable {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public Boolean isIn(List<WorkpackageAppBean> linkages) {
|
||||
return linkages.stream().map(WorkpackageAppBean::getFkWorkpackage).collect(Collectors.toSet()).contains(pkWorkpackage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
@@ -136,4 +144,18 @@ public class WorkpackageBean implements Serializable {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<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="@{/css/style.css}">
|
||||
<link rel="stylesheet" type="text/css" th:href="@{/public/dynamicstyle.css}">
|
||||
@@ -15,7 +16,9 @@
|
||||
|
||||
<script type="text/javascript" th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/webjars/bootstrap/5.3.8/js/bootstrap.bundle.min.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/webjars/datatables/2.3.5/js/dataTables.dataTables.min.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/webjars/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>
|
||||
|
||||
@@ -7,13 +7,67 @@
|
||||
<font layout:fragment="title">Projekt</font>
|
||||
<ul layout:fragment="menu">
|
||||
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
|
||||
<a class="nav-link btn btn-secondary btn-white-text" th:href="@{/projectmanagement}">zur Projektübersicht</a>
|
||||
<a class="btn btn-outline-secondary" th:href="@{/projectmanagement}">zur Projektübersicht</a>
|
||||
</li>
|
||||
</ul>
|
||||
<main layout:fragment="content">
|
||||
<div th:text="${app.name}"></div>
|
||||
<div th:each="p : ${workpackages}" th:text="${p.name}"></div>
|
||||
TODO: assign app to workpackage and store it
|
||||
<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>
|
||||
81
src/main/resources/templates/projectmanagement/app/item.html
Normal file
81
src/main/resources/templates/projectmanagement/app/item.html
Normal 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>
|
||||
@@ -7,39 +7,66 @@
|
||||
<font layout:fragment="title">Projekt</font>
|
||||
<ul layout:fragment="menu">
|
||||
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
|
||||
<a class="nav-link btn btn-secondary btn-white-text" th:href="@{/projectmanagement}">zur Projektübersicht</a>
|
||||
<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="p-2">
|
||||
<table id="table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>URL</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="b : ${apps}">
|
||||
<td th:text="${b.name}"></td>
|
||||
<td th:text="${b.description}"></td>
|
||||
<td><a th:href="${b.repositoryUrl}" target="_blank">gitlab</a></td>
|
||||
<td><a th:href="@{/projectmanagement/app/{id}/assign(id=${b.pkApp})}">zuordnen</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var localeUrl = '[[@{/js/dataTables/de.json}]]';
|
||||
$("#table").DataTable({
|
||||
"language" : {
|
||||
"url" : localeUrl
|
||||
}
|
||||
<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>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -21,8 +22,8 @@
|
||||
</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}(id=${w.pkWorkpackage})}" th:text="${w.name}"></a>
|
||||
<a th:href="@{/projectmanagement/workpackage/{id}/apps(id=${w.pkWorkpackage})}" class="ms-auto">apps</a>
|
||||
<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>
|
||||
@@ -31,10 +32,11 @@
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-3 col-lg-2 dashboardcard">
|
||||
<div class="card">
|
||||
<div class="card-header">Alles</div>
|
||||
<div class="card-body">
|
||||
<a th:href="@{/projectmanagement/apps}">alle apps</a>
|
||||
<div 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>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<font layout:fragment="title">Projekt</font>
|
||||
<ul layout:fragment="menu">
|
||||
<li class="nav-item" sec:authorize="hasRole('timetrack_user')">
|
||||
<a class="nav-link btn btn-secondary btn-white-text" th:href="@{/projectmanagement}">abbrechen</a>
|
||||
<a class="btn btn-outline-secondary" th:href="@{/projectmanagement}">abbrechen</a>
|
||||
</li>
|
||||
</ul>
|
||||
<main layout:fragment="content">
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
<div class="col-2">Geldgeber</div>
|
||||
<div class="col-10">
|
||||
<select th:field="*{fkFunder}" class="select-field">
|
||||
<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>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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-secondary btn-white-text" th:href="@{/projectmanagement}">abbrechen</a>
|
||||
<a class="btn btn-outline-secondary" th:href="@{/projectmanagement}">abbrechen</a>
|
||||
</li>
|
||||
</ul>
|
||||
<main layout:fragment="content">
|
||||
|
||||
Reference in New Issue
Block a user