added notes

This commit is contained in:
Jörg Henke
2022-05-02 14:43:43 +02:00
parent 3e363cc4bb
commit 2f51c93cfe
10 changed files with 637 additions and 6 deletions

View File

@ -93,9 +93,7 @@ public class ContactController {
@RolesAllowed("timetrack_user") @RolesAllowed("timetrack_user")
@GetMapping(value = "/contact/delete/{id}") @GetMapping(value = "/contact/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model) { public String doDelete(@PathVariable Integer id, Model model) {
LOGGER.info("up to delete bean {}", id);
Integer amount = contactService.doDelete(id); Integer amount = contactService.doDelete(id);
LOGGER.info("deleted {} rows", amount);
return amount.equals(1) ? getList(model) : toItem(id, model); return amount.equals(1) ? getList(model) : toItem(id, model);
} }
} }

View File

@ -0,0 +1,19 @@
package de.jottyfan.timetrack.spring.note;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
/**
*
* @author henkej
*
*/
public interface INoteService {
public List<NoteBean> getList();
public Integer doUpsert(NoteBean bean);
public Integer doDelete(Integer pk);
public Integer getAmount();
public String getCurrentUser(HttpServletRequest request);
public NoteBean getBean(Integer id);
}

View File

@ -0,0 +1,106 @@
package de.jottyfan.timetrack.spring.note;
import java.io.Serializable;
import de.jottyfan.timetrack.db.note.enums.EnumCategory;
import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
/**
*
* @author henkej
*
*/
public class NoteBean implements Serializable, Comparable<NoteBean> {
private static final long serialVersionUID = 1L;
private Integer pk;
private String title;
private EnumCategory category;
private String content;
private EnumNotetype type;
public NoteBean() {
super();
this.pk = null;
}
public NoteBean(Integer pk) {
super();
this.pk = pk;
}
@Override
public int compareTo(NoteBean o) {
return o == null ? 0 : getPk().compareTo(o.getPk());
}
/**
* @return the pk
*/
public Integer getPk() {
return pk;
}
/**
* @param pk the pk to set
*/
public void setPk(Integer pk) {
this.pk = pk;
}
/**
* @return the title
*/
public String getTitle() {
return title;
}
/**
* @param title the title to set
*/
public void setTitle(String title) {
this.title = title;
}
/**
* @return the category
*/
public EnumCategory getCategory() {
return category;
}
/**
* @param category the category to set
*/
public void setCategory(EnumCategory category) {
this.category = category;
}
/**
* @return the content
*/
public String getContent() {
return content;
}
/**
* @param content the content to set
*/
public void setContent(String content) {
this.content = content;
}
/**
* @return the type
*/
public EnumNotetype getType() {
return type;
}
/**
* @param type the type to set
*/
public void setType(EnumNotetype type) {
this.type = type;
}
}

View File

@ -0,0 +1,83 @@
package de.jottyfan.timetrack.spring.note;
import java.util.Arrays;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import de.jottyfan.timetrack.db.note.enums.EnumCategory;
import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
/**
*
* @author henkej
*
*/
@Controller
public class NoteController {
private static final Logger LOGGER = LogManager.getLogger(NoteController.class);
private final HttpServletRequest request;
@Autowired
private INoteService noteService;
@Autowired
public NoteController(HttpServletRequest request) {
this.request = request;
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/list")
public String getList(Model model) {
List<NoteBean> list = noteService.getList();
model.addAttribute("noteList", list);
return "note/list";
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/add", method = RequestMethod.GET)
public String toAdd(Model model) {
return toItem(null, model);
}
@RolesAllowed("timetrack_user")
@GetMapping("/note/edit/{id}")
public String toItem(@PathVariable Integer id, Model model) {
NoteBean bean = noteService.getBean(id);
if (bean == null) {
bean = new NoteBean(); // the add case
}
model.addAttribute("noteBean", bean);
model.addAttribute("types", Arrays.asList(EnumNotetype.values()));
model.addAttribute("categories", Arrays.asList(EnumCategory.values()));
return "note/item";
}
@RolesAllowed("timetrack_user")
@RequestMapping(value = "/note/upsert", method = RequestMethod.POST)
public String doUpsert(Model model, @ModelAttribute NoteBean bean) {
Integer amount = noteService.doUpsert(bean);
return amount.equals(1) ? getList(model) : toItem(bean.getPk(), model);
}
@RolesAllowed("timetrack_user")
@GetMapping(value = "/note/delete/{id}")
public String doDelete(@PathVariable Integer id, Model model) {
Integer amount = noteService.doDelete(id);
return amount.equals(1) ? getList(model) : toItem(id, model);
}
}

View File

@ -0,0 +1,194 @@
package de.jottyfan.timetrack.spring.note.impl;
import static de.jottyfan.timetrack.db.note.Tables.T_NOTE;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertValuesStep4;
import org.jooq.Record1;
import org.jooq.Record5;
import org.jooq.SelectConditionStep;
import org.jooq.SelectJoinStep;
import org.jooq.UpdateConditionStep;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.timetrack.db.note.enums.EnumCategory;
import de.jottyfan.timetrack.db.note.enums.EnumNotetype;
import de.jottyfan.timetrack.db.note.tables.records.TNoteRecord;
import de.jottyfan.timetrack.spring.note.NoteBean;
/**
*
* @author jotty
*
*/
@Repository
public class NoteGateway {
private static final Logger LOGGER = LogManager.getLogger(NoteGateway.class);
private final DSLContext jooq;
public NoteGateway(@Autowired DSLContext jooq) throws Exception {
this.jooq = jooq;
}
public DSLContext getJooq() {
return this.jooq;
}
/**
* get sorted list of notes
*
* @return a list (an empty one at least)
* @throws SQLException
* @throws ClassNotFoundException
* @throws DataAccessException
*/
public List<NoteBean> getAll() throws DataAccessException, ClassNotFoundException, SQLException {
SelectJoinStep<Record5<Integer, String, EnumCategory, String, EnumNotetype>> sql = getJooq()
// @formatter:off
.select(T_NOTE.PK,
T_NOTE.TITLE,
T_NOTE.CATEGORY,
T_NOTE.CONTENT,
T_NOTE.NOTETYPE)
.from(T_NOTE);
// @formatter:on
LOGGER.debug("{}", sql.toString());
List<NoteBean> list = new ArrayList<>();
for (Record5<Integer, String, EnumCategory, String, EnumNotetype> r : sql.fetch()) {
NoteBean bean = new NoteBean(r.get(T_NOTE.PK));
bean.setTitle(r.get(T_NOTE.TITLE));
bean.setCategory(r.get(T_NOTE.CATEGORY));
bean.setContent(r.get(T_NOTE.CONTENT));
bean.setType(r.get(T_NOTE.NOTETYPE));
list.add(bean);
}
list.sort((o1, o2) -> o1 == null ? 0 : o1.compareTo(o2));
return list;
}
/**
* delete a note from the database
*
* @param pk the id of the contact
* @return the number of affected database rows, should be 1
* @throws SQLException
* @throws ClassNotFoundException
* @throws DataAccessException
*/
public Integer delete(Integer pk) throws DataAccessException, ClassNotFoundException, SQLException {
DeleteConditionStep<TNoteRecord> sql = getJooq()
// @formatter:off
.deleteFrom(T_NOTE)
.where(T_NOTE.PK.eq(pk));
// @formatter:on
LOGGER.debug("{}", sql.toString());
return sql.execute();
}
/**
* add a note to the database
*
* @param bean the contact information
* @return the number of affected database rows, should be 1
* @throws SQLException
* @throws ClassNotFoundException
* @throws DataAccessException
*/
public Integer add(NoteBean bean) throws DataAccessException, ClassNotFoundException, SQLException {
InsertValuesStep4<TNoteRecord, String, EnumCategory, String, EnumNotetype> sql = getJooq()
// @formatter:off
.insertInto(T_NOTE,
T_NOTE.TITLE,
T_NOTE.CATEGORY,
T_NOTE.CONTENT,
T_NOTE.NOTETYPE)
.values(bean.getTitle(), bean.getCategory(), bean.getContent(), bean.getType());
// @formatter:on
LOGGER.debug("{}", sql.toString());
return sql.execute();
}
/**
* update a note in the database, referencing its pk
*
* @param bean the contact information
* @return the number of affected database rows, should be 1
* @throws SQLException
* @throws ClassNotFoundException
* @throws DataAccessException
*/
public Integer update(NoteBean bean) throws DataAccessException, ClassNotFoundException, SQLException {
UpdateConditionStep<TNoteRecord> sql = getJooq()
// @formatter:off
.update(T_NOTE)
.set(T_NOTE.TITLE, bean.getTitle())
.set(T_NOTE.CATEGORY, bean.getCategory())
.set(T_NOTE.CONTENT, bean.getContent())
.set(T_NOTE.NOTETYPE, bean.getType())
.where(T_NOTE.PK.eq(bean.getPk()));
// @formatter:on
LOGGER.debug("{}", sql.toString());
return sql.execute();
}
/**
* get number of entries in t_note
*
* @return number of entries
* @throws SQLException
* @throws ClassNotFoundException
* @throws DataAccessException
*/
public Integer getAmount() throws DataAccessException, ClassNotFoundException, SQLException {
SelectJoinStep<Record1<Integer>> sql = getJooq()
// @formatter:off
.selectCount()
.from(T_NOTE);
// @formatter:on
LOGGER.debug("{}", sql.toString());
return sql.fetchOne(DSL.count());
}
/**
* get all enum types
*
* @return list of enum types
*/
public List<EnumNotetype> getTypes() {
return new ArrayList<>(Arrays.asList(EnumNotetype.values()));
}
public NoteBean getBean(Integer id) {
SelectConditionStep<Record5<Integer, String, EnumCategory, String, EnumNotetype>> sql = getJooq()
// @formatter:off
.select(T_NOTE.PK,
T_NOTE.TITLE,
T_NOTE.CATEGORY,
T_NOTE.CONTENT,
T_NOTE.NOTETYPE)
.from(T_NOTE)
.where(T_NOTE.PK.eq(id));
// @formatter:on
LOGGER.debug("{}", sql.toString());
for (Record5<Integer, String, EnumCategory, String, EnumNotetype> r : sql.fetch()) {
NoteBean bean = new NoteBean(r.get(T_NOTE.PK));
bean.setTitle(r.get(T_NOTE.TITLE));
bean.setCategory(r.get(T_NOTE.CATEGORY));
bean.setContent(r.get(T_NOTE.CONTENT));
bean.setType(r.get(T_NOTE.NOTETYPE));
return bean;
}
return null;
}
}

View File

@ -0,0 +1,83 @@
package de.jottyfan.timetrack.spring.note.impl;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.keycloak.KeycloakSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import de.jottyfan.timetrack.spring.note.INoteService;
import de.jottyfan.timetrack.spring.note.NoteBean;
/**
*
* @author henkej
*
*/
@Service
@Transactional(transactionManager = "transactionManager")
public class NoteService implements INoteService {
private static final Logger LOGGER = LogManager.getLogger(NoteService.class);
@Autowired
private DSLContext dsl;
@Override
public String getCurrentUser(HttpServletRequest request) {
KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
return ksc == null ? "" : ksc.getIdToken().getPreferredUsername();
}
@Override
public List<NoteBean> getList() {
try {
return new NoteGateway(dsl).getAll();
} catch (Exception e) {
LOGGER.error(e);
return new ArrayList<>();
}
}
@Override
public Integer doUpsert(NoteBean bean) {
try {
NoteGateway gw = new NoteGateway(dsl);
return bean.getPk() == null ? gw.add(bean) : gw.update(bean);
} catch (Exception e) {
LOGGER.error(e);
return 0;
}
}
@Override
public Integer doDelete(Integer pk) {
try {
return new NoteGateway(dsl).delete(pk);
} catch (Exception e) {
LOGGER.error(e);
return 0;
}
}
@Override
public Integer getAmount() {
return getList().size();
}
@Override
public NoteBean getBean(Integer id) {
try {
return new NoteGateway(dsl).getBean(id);
} catch (Exception e) {
LOGGER.error(e);
return null;
}
}
}

View File

@ -4,13 +4,17 @@ html {
body { body {
background-color: #99c1f1; background-color: #99c1f1;
height: calc(100% - 76px); height: calc(100% - 56px);
} }
.float-right { .float-right {
float: right; float: right;
} }
.noty {
background-image: linear-gradient(to bottom, #ffc, #ee0) !important;
}
.glassy { .glassy {
background-color: rgba(1,1,1,0.1); background-color: rgba(1,1,1,0.1);
} }
@ -128,6 +132,10 @@ body {
.version { .version {
font-size: small; font-size: small;
color: silver; color: silver;
position: absolute;
padding-top: 36px;
padding-left: 22px;
z-index: 0;
} }
.fc-content { .fc-content {

View File

@ -20,9 +20,8 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-light bg-light static-top"> <nav class="navbar navbar-expand-lg navbar-light bg-light static-top">
<div class="container-fluid" style="width: 98%"> <div class="container-fluid" style="width: 98%">
<a class="navbar-brand" th:href="@{/}"> <i class="fa fa-calendar-alt"></i> Timetrack <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> <div class="version" th:text="${@manifestBean.getVersion()}"></div>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive"
aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation"> aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -33,6 +32,7 @@
data-bs-toggle="dropdown" aria-expanded="false"> Module </a> data-bs-toggle="dropdown" aria-expanded="false"> Module </a>
<ul class="dropdown-menu dropdown-menu-light" aria-labelledby="navbarScrollingDropdown"> <ul class="dropdown-menu dropdown-menu-light" aria-labelledby="navbarScrollingDropdown">
<li><a class="dropdown-item" th:href="@{/contact/list}">Kontakte</a></li> <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><hr /></li> <li><hr /></li>
<li><a class="dropdown-item" th:href="@{/logout}">[[${currentUser}]] abmelden</a></li> <li><a class="dropdown-item" th:href="@{/logout}">[[${currentUser}]] abmelden</a></li>
</ul></li> </ul></li>

View File

@ -0,0 +1,64 @@
<!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>Notiz aktualisieren</title>
</head>
<body>
<ul layout:fragment="menu">
</ul>
<main layout:fragment="content">
<div class="container formpane">
<form th:action="@{/note/upsert}" th:object="${noteBean}" method="post">
<div class="row mb-3">
<label for="inputPk" class="col-sm-2 col-form-label">Inhalt von Eintrag</label>
<div class="col-sm-10">
<input id="inputPk" type="text" th:field="*{pk}" class="form-control" readonly="readonly" />
</div>
</div>
<div class="row mb-3">
<label for="inputTitle" class="col-sm-2 col-form-label">Titel</label>
<div class="col-sm-10">
<input id="inputTitle" type="text" th:field="*{title}" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label for="inputCategory" class="col-sm-2 col-form-label">Kategorie</label>
<div class="col-sm-10">
<select id="inputCategory" class="form-control select2-single" th:field="*{category}">
<option th:each="i : ${categories}" th:value="${i}" th:text="${i.literal}"></option>
</select>
</div>
</div>
<div class="row mb-3">
<label for="inputContent" class="col-sm-2 col-form-label">Inhalt</label>
<div class="col-sm-10">
<input id="inputContent" type="text" th:field="*{content}" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label for="inputType" class="col-sm-2 col-form-label">Typ</label>
<div class="col-sm-10">
<select id="inputType" class="form-control select2-single" th:field="*{type}">
<option th:each="i : ${types}" th:value="${i}" th:text="${i.literal}"></option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-2">Änderung</div>
<div class="col-sm-10">
<button type="submit" class="btn btn-success">speichern</button>
<button type="submit" class="btn btn-secondary" th:formaction="@{/note/list}">abbrechen</button>
<div class="dropdown float-right" th:if="${noteBean.pk != null}" sec:authorize="hasRole('timetrack_user')">
<button class="btn btn-danger dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Eintrag löschen</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" th:href="@{/note/delete/{id}(id=${noteBean.pk})}">endgültig löschen</a></li>
</ul>
</div>
</div>
</div>
</form>
</div>
</main>
</body>
</html>

View File

@ -0,0 +1,76 @@
<!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>Notizen</title>
</head>
<body>
<ul layout:fragment="menu">
<li class="nav-item" sec:authorize="hasRole('timetrack_user')"><a class="nav-link" th:href="@{/note/add}">Neue Notiz
anlegen</a></li>
</ul>
<main layout:fragment="content">
<div class="accordion" id="acdiv">
<div class="accordion-item noty">
<h2 class="accordion-header" id="headingDashboard">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#dashboard" aria-expanded="true"
aria-controls="dashboard">Dashboard</button>
</h2>
<div id="dashboard" class="accordion-collapse collapse show" aria-labelledby="headingDashboard" data-bs-parent="#acdiv">
<div class="accordion-body">
<div class="row row-cols-12 ro-cols-lg-3 ro-cols-md-2 ro-cols-sd-1 g-4" style="margin: 8px">
<div class="col" th:each="note : ${noteList}">
<div class="card text-dark bg-light shadow" style="width: 100%">
<div class="card-header text-center">
<font th:text="${note.category}" style="font-size: larger">:</font> <font th:text="${note.title}"
style="font-size: larger; font-weight: bolder"></font>
</div>
<div class="card-body">
<div class="d-flex justify-content-center align-items-center">
<pre th:text="${note.content}"></pre>
<a th:href="@{/note/edit/{id}(id=${note.pk})}" sec:authorize="hasRole('timetrack_user')" style="margin-left: 8px;">
<i class="fa fa-edit"></i>
</a>
</div>
</div>
<div class="card-footer">
<span th:text="${note.type}"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item glassy">
<h2 class="accordion-header" id="headingTable">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#list"
aria-expanded="false" aria-controls="list">Liste</button>
</h2>
<div id="list" class="accordion-collapse collapse" aria-labelledby="headingTable" data-bs-parent="#acdiv">
<div class="accordion-body" style="background-color: white">
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Titel</th>
<th>Kategorie</th>
<th>Inhalt</th>
<th>Typ</th>
</tr>
</thead>
<tbody>
<tr th:each="note : ${noteList}">
<td><a th:href="@{/note/edit/{id}(id=${note.pk})}"><span th:text="${note.title}"></span></a></td>
<td><a th:href="@{/note/edit/{id}(id=${note.pk})}"><span th:text="${note.category}"></span></a></td>
<td><a th:href="@{/note/edit/{id}(id=${note.pk})}"><span th:text="${note.content}"></span></a></td>
<td><a th:href="@{/note/edit/{id}(id=${note.pk})}"><span th:text="${note.type}"></span></a></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>