added outlay

This commit is contained in:
Jottyfan 2024-08-19 17:23:41 +02:00
parent b10765fe89
commit 32ef8a67d9
10 changed files with 667 additions and 2 deletions

View File

@ -8,7 +8,7 @@ plugins {
} }
group = 'de.jottyfan.camporganizer' group = 'de.jottyfan.camporganizer'
version = '0.8.4' version = '0.8.5'
description = """CampOrganizer2""" description = """CampOrganizer2"""

View File

@ -0,0 +1,66 @@
package de.jottyfan.camporganizer.module.business.outlay;
import java.security.Principal;
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.camporganizer.module.business.outlay.model.OutlayBean;
import de.jottyfan.camporganizer.module.camplist.CommonController;
import jakarta.annotation.security.RolesAllowed;
import jakarta.validation.Valid;
/**
*
* @author jotty
*
*/
@Controller
@RolesAllowed({ "business_outlay" })
public class OutlayController extends CommonController {
@Autowired
private OutlayService service;
@GetMapping("/business/outlay")
public String getOutlayDashboard(Model model, Principal principal) {
model.addAttribute("list", service.getListOfUser(super.getCurrentUser(principal)));
return "/business/outlay/list";
}
@GetMapping("/business/outlay/add")
public String loadAddMask(Model model, Principal principal) {
model.addAttribute("bean", new OutlayBean().withUser(super.getCurrentUser(principal)));
model.addAttribute("camps", service.getAllCamps());
return "/business/outlay/editor";
}
@GetMapping("/business/outlay/edit/{id}")
public String loadAddMask(@PathVariable("id") Integer id, Model model, Principal principal) {
model.addAttribute("bean", service.getBeanIfAllowedFor(super.getCurrentUser(principal), id));
model.addAttribute("camps", service.getAllCamps());
return "/business/outlay/editor";
}
@PostMapping("/business/outlay/save")
public String save(@Valid @ModelAttribute("bean") OutlayBean bean, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("camps", service.getAllCamps());
return "/business/outlay/editor";
}
service.save(bean);
return "redirect:/business/outlay";
}
@GetMapping("/business/outlay/delete/{id}")
public String delete(@PathVariable("id") Integer id, Principal principal) {
service.deleteIfAllowedFor(super.getCurrentUser(principal), id);
return "redirect:/business/outlay";
}
}

View File

@ -0,0 +1,191 @@
package de.jottyfan.camporganizer.module.business.outlay;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_CAMP;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_LOCATION;
import static de.jottyfan.camporganizer.db.jooq.Tables.T_SALES;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Iterator;
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.InsertValuesStep8;
import org.jooq.Record10;
import org.jooq.Record4;
import org.jooq.Record7;
import org.jooq.SelectConditionStep;
import org.jooq.SelectSeekStep1;
import org.jooq.UpdateConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import de.jottyfan.camporganizer.db.jooq.tables.records.TSalesRecord;
import de.jottyfan.camporganizer.module.business.outlay.model.CampBean;
import de.jottyfan.camporganizer.module.business.outlay.model.OutlayBean;
import jakarta.validation.Valid;
/**
*
* @author jotty
*
*/
@Repository
@Transactional(transactionManager = "transactionManager")
public class OutlayRepository {
private static final Logger LOGGER = LogManager.getLogger(OutlayRepository.class);
@Autowired
private DSLContext jooq;
/**
* get all camps from the database
*
* @return a list of camps; an empty list at least
*/
public List<CampBean> getAllCamps() {
SelectSeekStep1<Record4<Integer, String, LocalDateTime, String>, LocalDateTime> sql = jooq
// @formatter:off
.select(T_CAMP.PK, T_CAMP.NAME, T_CAMP.ARRIVE, T_LOCATION.NAME)
.from(T_CAMP)
.leftJoin(T_LOCATION).on(T_LOCATION.PK.eq(T_CAMP.FK_LOCATION))
.orderBy(T_CAMP.ARRIVE.desc());
// @formatter:on
LOGGER.trace(sql.toString());
List<CampBean> list = new ArrayList<>();
Iterator<Record4<Integer, String, LocalDateTime, String>> i = sql.fetch().iterator();
while (i.hasNext()) {
Record4<Integer, String, LocalDateTime, String> r = i.next();
String name = String
.format("%s %s %d", r.get(T_CAMP.NAME), r.get(T_LOCATION.NAME), r.get(T_CAMP.ARRIVE).getYear()).trim();
list.add(CampBean.of(r.get(T_CAMP.PK)).withCampname(name));
}
return list;
}
public List<OutlayBean> getListOf(String username) {
SelectConditionStep<Record7<Integer, String, String, LocalDateTime, String, BigDecimal, LocalDateTime>> sql = jooq
// @formatter:off
.select(T_SALES.PK,
T_SALES.TRADER,
T_CAMP.NAME,
T_CAMP.ARRIVE,
T_LOCATION.NAME,
T_SALES.CASH,
T_SALES.BUYDATE)
.from(T_SALES)
.leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_SALES.FK_CAMP))
.leftJoin(T_LOCATION).on(T_LOCATION.PK.eq(T_CAMP.FK_LOCATION))
.where(T_SALES.PROVIDER.eq(username));
// @formatter:on
LOGGER.trace(sql);
List<OutlayBean> list = new ArrayList<>();
Iterator<Record7<Integer, String, String, LocalDateTime, String, BigDecimal, LocalDateTime>> i = sql.fetch()
.iterator();
while (i.hasNext()) {
Record7<Integer, String, String, LocalDateTime, String, BigDecimal, LocalDateTime> r = i.next();
String campname = String
.format("%s %s %d", r.get(T_CAMP.NAME), r.get(T_LOCATION.NAME), r.get(T_CAMP.ARRIVE).getYear()).trim();
OutlayBean bean = new OutlayBean();
bean.setId(r.get(T_SALES.PK));
bean.setTrader(r.get(T_SALES.TRADER));
bean.setCampname(campname);
bean.setCash(r.get(T_SALES.CASH));
bean.setBuydate(r.get(T_SALES.BUYDATE));
list.add(bean);
}
return list;
}
public void addBean(@Valid OutlayBean bean) {
InsertValuesStep8<TSalesRecord, LocalDateTime, BigDecimal, Integer, String, String, String, String, String> sql = jooq
// @formatter:off
.insertInto(T_SALES,
T_SALES.BUYDATE,
T_SALES.CASH,
T_SALES.FK_CAMP,
T_SALES.INCREDIENTS,
T_SALES.PROVIDER,
T_SALES.RECIPENOTE,
T_SALES.RECIPENUMBER,
T_SALES.TRADER)
.values(bean.getBuydate(), bean.getCash(), bean.getFkCamp(), bean.getIngredients(), bean.getProvider(), bean.getRecipenote(), bean.getRecipenumber(), bean.getTrader());
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
public void updateBean(@Valid OutlayBean bean) {
UpdateConditionStep<TSalesRecord> sql = jooq
// @formatter:off
.update(T_SALES)
.set(T_SALES.BUYDATE, bean.getBuydate())
.set(T_SALES.CASH, bean.getCash())
.set(T_SALES.FK_CAMP, bean.getFkCamp())
.set(T_SALES.INCREDIENTS, bean.getIngredients())
.set(T_SALES.PROVIDER, bean.getProvider())
.set(T_SALES.RECIPENOTE, bean.getRecipenote())
.set(T_SALES.RECIPENUMBER, bean.getRecipenumber())
.set(T_SALES.TRADER, bean.getTrader())
.where(T_SALES.PK.eq(bean.getId()));
// @formatter:on
LOGGER.trace(sql);
sql.execute();
}
public OutlayBean getBeanIfAllowedFor(String username, Integer id) {
SelectConditionStep<Record10<Integer, String, Integer, BigDecimal, LocalDateTime, String, String, String, String, String>> sql = jooq
// @formatter:off
.select(T_SALES.PK,
T_SALES.TRADER,
T_SALES.FK_CAMP,
T_SALES.CASH,
T_SALES.BUYDATE,
T_SALES.INCREDIENTS,
T_SALES.PROVIDER,
T_SALES.RECIPENOTE,
T_SALES.RECIPENUMBER,
T_SALES.TRADER)
.from(T_SALES)
.leftJoin(T_CAMP).on(T_CAMP.PK.eq(T_SALES.FK_CAMP))
.leftJoin(T_LOCATION).on(T_LOCATION.PK.eq(T_CAMP.FK_LOCATION))
.where(T_SALES.PROVIDER.eq(username))
.and(T_SALES.PK.eq(id));
// @formatter:on
LOGGER.trace(sql);
Record10<Integer, String, Integer, BigDecimal, LocalDateTime, String, String, String, String, String> r = sql
.fetchOne();
if (r == null) {
return null;
} else {
OutlayBean bean = new OutlayBean();
bean.setId(r.get(T_SALES.PK));
bean.setBuydate(r.get(T_SALES.BUYDATE));
bean.setCash(r.get(T_SALES.CASH));
bean.setFkCamp(r.get(T_SALES.FK_CAMP));
bean.setIngredients(r.get(T_SALES.INCREDIENTS));
bean.setProvider(r.get(T_SALES.PROVIDER));
bean.setRecipenote(r.get(T_SALES.RECIPENOTE));
bean.setRecipenumber(r.get(T_SALES.RECIPENUMBER));
bean.setTrader(r.get(T_SALES.TRADER));
return bean;
}
}
public void deleteBeanIfAllowedFor(String username, Integer id) {
DeleteConditionStep<TSalesRecord> sql = jooq
// @formatter:off
.deleteFrom(T_SALES)
.where(T_SALES.PROVIDER.eq(username))
.and(T_SALES.PK.eq(id));
// @formatter:off
LOGGER.trace(sql);
sql.execute();
}
}

View File

@ -0,0 +1,46 @@
package de.jottyfan.camporganizer.module.business.outlay;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.camporganizer.module.business.outlay.model.CampBean;
import de.jottyfan.camporganizer.module.business.outlay.model.OutlayBean;
import jakarta.validation.Valid;
/**
*
* @author jotty
*
*/
@Service
public class OutlayService {
@Autowired
private OutlayRepository repository;
public List<OutlayBean> getListOfUser(String username) {
return repository.getListOf(username);
}
public List<CampBean> getAllCamps() {
return repository.getAllCamps();
}
public void save(@Valid OutlayBean bean) {
if (bean.getId() != null) {
repository.updateBean(bean);
} else {
repository.addBean(bean);
}
}
public OutlayBean getBeanIfAllowedFor(String username, Integer id) {
return repository.getBeanIfAllowedFor(username, id);
}
public void deleteIfAllowedFor(String username, Integer id) {
repository.deleteBeanIfAllowedFor(username, id);
}
}

View File

@ -0,0 +1,51 @@
package de.jottyfan.camporganizer.module.business.outlay.model;
import java.io.Serializable;
/**
*
* @author jotty
*
*/
public class CampBean implements Serializable {
private static final long serialVersionUID = 1L;
private final Integer id;
private String campname;
private CampBean(Integer id) {
super();
this.id = id;
}
public static final CampBean of(Integer id) {
return new CampBean(id);
}
public CampBean withCampname(String campname) {
this.campname = campname;
return this;
}
/**
* @return the campname
*/
public String getCampname() {
return campname;
}
/**
* @param campname the campname to set
*/
public void setCampname(String campname) {
this.campname = campname;
}
/**
* @return the id
*/
public Integer getId() {
return id;
}
}

View File

@ -0,0 +1,180 @@
package de.jottyfan.camporganizer.module.business.outlay.model;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
*
* @author jotty
*
*/
public class OutlayBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
@NotBlank
private String trader;
@NotNull
private Integer fkCamp;
private String campname;
private String provider;
@NotNull
private BigDecimal cash;
@DateTimeFormat(pattern = "yyyy-MM-dd'T'hh:mm:ss")
private LocalDateTime buydate;
private String recipenumber;
private String recipenote;
private String ingredients;
public OutlayBean withUser(String provider) {
this.provider = provider;
return this;
}
/**
* @return the id
*/
public Integer getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(Integer id) {
this.id = id;
}
/**
* @return the trader
*/
public String getTrader() {
return trader;
}
/**
* @param trader the trader to set
*/
public void setTrader(String trader) {
this.trader = trader;
}
/**
* @return the campname
*/
public String getCampname() {
return campname;
}
/**
* @param campname the campname to set
*/
public void setCampname(String campname) {
this.campname = campname;
}
/**
* @return the cash
*/
public BigDecimal getCash() {
return cash;
}
/**
* @param cash the cash to set
*/
public void setCash(BigDecimal cash) {
this.cash = cash;
}
/**
* @return the buydate
*/
public LocalDateTime getBuydate() {
return buydate;
}
/**
* @param buydate the buydate to set
*/
public void setBuydate(LocalDateTime buydate) {
this.buydate = buydate;
}
/**
* @return the provider
*/
public String getProvider() {
return provider;
}
/**
* @param provider the provider to set
*/
public void setProvider(String provider) {
this.provider = provider;
}
/**
* @return the recipenumber
*/
public String getRecipenumber() {
return recipenumber;
}
/**
* @param recipenumber the recipenumber to set
*/
public void setRecipenumber(String recipenumber) {
this.recipenumber = recipenumber;
}
/**
* @return the recipenote
*/
public String getRecipenote() {
return recipenote;
}
/**
* @param recipenote the recipenote to set
*/
public void setRecipenote(String recipenote) {
this.recipenote = recipenote;
}
/**
* @return the ingredients
*/
public String getIngredients() {
return ingredients;
}
/**
* @param ingredients the ingredients to set
*/
public void setIngredients(String ingredients) {
this.ingredients = ingredients;
}
/**
* @return the fkCamp
*/
public Integer getFkCamp() {
return fkCamp;
}
/**
* @param fkCamp the fkCamp to set
*/
public void setFkCamp(Integer fkCamp) {
this.fkCamp = fkCamp;
}
}

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Camp Organizer Business</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<th:block layout:fragment="content">
<div class="mainpage">
<div class="container" style="max-width: 100%" sec:authorize="hasRole('business_outlay')">
<div class="alert alert-danger" th:unless="${bean}">Die Bearbeitung der Abrechnung ist nur dem Auslegenden erlaubt.</div>
<form th:action="@{/business/outlay/save}" th:object="${bean}" method="post" th:if="${bean}">
<input type="hidden" th:field="*{id}" />
<div class="row g-2">
<div class="col-lg-1 col-md-2 col-sm-12">Auslegender</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<input type="text" th:field="*{provider}" class="form-control" readonly="readonly" />
</div>
<div class="col-lg-1 col-md-2 col-sm-12">Händler / Shop</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<span class="error" th:each="error : ${#fields.errors('trader')}">[[${error}]]<br /></span> <input type="text" th:field="*{trader}" class="form-control" />
</div>
<div class="col-lg-1 col-md-2 col-sm-12">Freizeit</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<span class="error" th:each="error : ${#fields.errors('fkCamp')}">[[${error}]]<br /></span> <select id="inputCamp" th:field="*{fkCamp}"
th:class="${'form-select ' + (#fields.hasErrors('fkCamp') ? 'inputerror' : '')}">
<option value="">--- bitte wählen ---</option>
<option th:each="l : ${camps}" th:value="${l.id}" th:text="${l.campname}"></option>
</select>
<script type="text/javascript">
$(document).ready(function() {
$("#inputCamp").select2();
});
</script>
</div>
<div class="col-lg-1 col-md-2 col-sm-12">Betrag</div>
<div class="col-lg-10 col-md-9 col-sm-11">
<span class="error" th:each="error : ${#fields.errors('cash')}">[[${error}]]<br /></span> <input type="number" step="0.01" th:field="*{cash}" class="form-control" />
</div>
<div class="col-lg-1 col-md-1 col-sm-1 h3"></div>
<div class="col-lg-1 col-md-2 col-sm-12">Tag / Uhrzeit</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<input type="datetime-local" th:field="*{buydate}" class="form-control" step="1" />
</div>
<div class="col-lg-1 col-md-2 col-sm-12">Kassenzettelnummer</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<input type="text" th:field="*{recipenumber}" class="form-control" />
</div>
<div class="col-lg-1 col-md-2 col-sm-12">Kurzbeschreibung</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<textarea th:field="*{ingredients}" class="form-control"></textarea>
</div>
<div class="col-lg-1 col-md-2 col-sm-12">Bemerkungen</div>
<div class="col-lg-11 col-md-10 col-sm-12">
<textarea th:field="*{recipenote}" class="form-control"></textarea>
</div>
<div class="col-12 text-center">
<button type="submit" class="btn btn-success">Speichern</button>
<a th:href="@{/business/outlay}" class="btn btn-outline-secondary">Abbrechen</a>
<button type="button" class="btn btn-outline-danger" th:if="${bean.id}" data-bs-toggle="modal" data-bs-target="#deleteModal">Löschen</button>
<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 alert alert-danger">
<h1 class="modal-title fs-5" id="deleteModalLabel">Löschen der Abrechnung</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Willst du wirklich diesen Eintrag mit einem Betrag von <span th:text="${#numbers.formatDecimal(bean.cash, 1, 2, 'COMMA')}"></span> € löschen? Diese Daten gehen dauerhaft verloren.
</div>
<div class="modal-footer">
<a th:href="@{/business/outlay/delete/{id}(id=${bean.id})}" class="btn btn-danger">endgültig löschen</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">abbrechen</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>Camp Organizer Business</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<th:block layout:fragment="content">
<div class="mainpage">
<div class="container" style="max-width: 100%" sec:authorize="hasRole('business_outlay')">
<table id="table" class="table table-striped">
<thead>
<tr>
<th>Händler / Shop</th>
<th>Freizeit</th>
<th>Betrag</th>
<th>Tag / Uhrzeit</th>
</tr>
</thead>
<tbody>
<tr th:each="o : ${list}">
<td><a th:href="@{/business/outlay/edit/{id}(id=${o.id})}" th:text="${o.trader}"></a></td>
<td><a th:href="@{/business/outlay/edit/{id}(id=${o.id})}" th:text="${o.campname}"></a></td>
<td><a th:href="@{/business/outlay/edit/{id}(id=${o.id})}" th:text="${#numbers.formatDecimal(o.cash, 1, 2, 'COMMA')} + ' €'"></a></td>
<td><a th:href="@{/business/outlay/edit/{id}(id=${o.id})}" th:text="${#temporals.format(o.buydate, 'dd.MM.yyyy, HH:mm.ss')}"></a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4"><a th:href="@{/business/outlay/add}" class="btn btn-outline-primary form-control">neue Rechnung eintragen</a></td>
</tr>
</tfoot>
</table>
<script>
$(document).ready(function() {
$("#table").DataTable({
language : locale_de
});
});
</script>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -27,7 +27,7 @@
</div> </div>
--> -->
<h2 class="headlinefont center">Willkommen bei den</h2> <h2 class="headlinefont center">Willkommen bei den</h2>
<h1 class="titlefont center">MITTELPUNKTFREIZEITEN</h1> <h1 class="titlefont center">MITTELPUNKT-FREIZEITEN</h1>
<br /> <br />
<br /> <br />
<div class="blocktext"> <div class="blocktext">

View File

@ -75,6 +75,7 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a th:href="@{/business}" class="dropdown-item menufont">Freizeitübersicht</a></li> <li><a th:href="@{/business}" class="dropdown-item menufont">Freizeitübersicht</a></li>
<li><a th:href="@{/business/bookings}" class="dropdown-item menufont" sec:authorize="hasRole('business_booking')">Buchungsübersicht</a></li> <li><a th:href="@{/business/bookings}" class="dropdown-item menufont" sec:authorize="hasRole('business_booking')">Buchungsübersicht</a></li>
<li><a th:href="@{/business/outlay}" class="dropdown-item menufont" sec:authorize="hasRole('business_outlay')">Auslagen / Rechnungen</a>
<li><a th:href="@{/business/privileges}" class="dropdown-item menufont" sec:authorize="hasRole('admin')">Nutzerverwaltung</a></li> <li><a th:href="@{/business/privileges}" class="dropdown-item menufont" sec:authorize="hasRole('admin')">Nutzerverwaltung</a></li>
</ul> </ul>
</div> </div>