#18 Period overview: improvements

This commit is contained in:
2021-03-05 20:15:05 +01:00
parent 2aa0f7fa9c
commit 61cd310fb0
11 changed files with 243 additions and 74 deletions

View File

@@ -23,7 +23,7 @@ orderByExpression
regularExpression
: (stringExpression | intExpression | booleanExpression | dateExpression) ;
stringExpression
: field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ;
: field=IDENTIFIER operator=STRING_OPERATOR value=(STRING_VALUE | ACCOUNT_TYPE_VALUE) ;
intExpression
: field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ;

View File

@@ -7,24 +7,38 @@ import de.financer.model.*;
import java.util.Arrays;
public enum FieldMapping {
AMOUNT("amount", Transaction_.AMOUNT, Transaction.class, NoopJoinHandler.class, JoinKey
.of(Transaction.class), null, IntHandler.class),
AMOUNT("amount", Transaction_.AMOUNT, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, IntHandler.class),
PERIOD("period", Period_.ID, Period.class, PeriodJoinHandler.class, JoinKey.of(Period.class), null, PeriodConstHandler.class),
PERIOD("period", Period_.ID, Period.class, PeriodJoinHandler.class,
JoinKey.of(Period.class), null, PeriodConstHandler.class),
FROM_ACCOUNT("fromAccount", Account_.KEY, Account.class, FromAccountJoinHandler.class, JoinKey
.of(Account.class, "FROM"), null, StringHandler.class),
/**
* ONLY FOR INTERNAL USAGE - NOT EXPOSED TO USER
*/
PERIOD_ID("periodId", Period_.ID, Period.class, PeriodJoinHandler.class,
JoinKey.of(Period.class), null, PeriodIdHandler.class),
TO_ACCOUNT("toAccount", Account_.KEY, Account.class, ToAccountJoinHandler.class, JoinKey
.of(Account.class, "TO"), null, StringHandler.class),
FROM_ACCOUNT("fromAccount", Account_.KEY, Account.class, FromAccountJoinHandler.class,
JoinKey.of(Account.class, "FROM"), null, StringHandler.class),
FROM_ACCOUNT_GROUP("fromAccountGroup", AccountGroup_.NAME, AccountGroup.class,
AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "FROM"), FROM_ACCOUNT, StringHandler.class),
TO_ACCOUNT("toAccount", Account_.KEY, Account.class, ToAccountJoinHandler.class,
JoinKey.of(Account.class, "TO"), null, StringHandler.class),
TO_ACCOUNT_GROUP("toAccountGroup", AccountGroup_.NAME, AccountGroup.class,
AccountGroupJoinHandler.class, JoinKey.of(AccountGroup.class, "TO"), TO_ACCOUNT, StringHandler.class),
FROM_ACCOUNT_GROUP("fromAccountGroup", AccountGroup_.NAME, AccountGroup.class, AccountGroupJoinHandler.class,
JoinKey.of(AccountGroup.class, "FROM"), FROM_ACCOUNT, StringHandler.class),
DATE("date", Transaction_.DATE, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class), null, DateHandler.class),
TO_ACCOUNT_GROUP("toAccountGroup", AccountGroup_.NAME, AccountGroup.class, AccountGroupJoinHandler.class,
JoinKey.of(AccountGroup.class, "TO"), TO_ACCOUNT, StringHandler.class),
FROM_ACCOUNT_TYPE("fromAccountType", Account_.TYPE, Account.class, FromAccountJoinHandler.class,
JoinKey.of(Account.class, "FROM"), null, AccountTypeHandler.class),
TO_ACCOUNT_TYPE("toAccountType", Account_.TYPE, Account.class, ToAccountJoinHandler.class,
JoinKey.of(Account.class, "TO"), null, AccountTypeHandler.class),
DATE("date", Transaction_.DATE, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, DateHandler.class),
RECURRING("recurring", Transaction_.RECURRING_TRANSACTION, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, NotNullSyntheticHandler.class),
@@ -32,10 +46,11 @@ public enum FieldMapping {
TAX_RELEVANT("taxRelevant", Transaction_.TAX_RELEVANT, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, BooleanHandler.class),
HAS_FILE("hasFile", File_.ID, File.class, FileJoinHandler.class, JoinKey.of(File.class), null, NotNullSyntheticHandler.class),
HAS_FILE("hasFile", File_.ID, File.class, FileJoinHandler.class,
JoinKey.of(File.class), null, NotNullSyntheticHandler.class),
DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.class, JoinKey.of(Transaction.class),
null, StringHandler.class);
DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, StringHandler.class);
private final String fieldName;

View File

@@ -0,0 +1,32 @@
package de.financer.fql.field_handler;
import de.financer.fql.FQLException;
import de.financer.fql.FieldMapping;
import de.financer.fql.join_handler.JoinKey;
import de.financer.model.AccountType;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import java.util.Map;
public class AccountTypeHandler implements FieldHandler<String> {
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, String value) {
return criteriaBuilder
.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), toAccountType(FieldHandlerUtils.removeQuotes(value)));
}
private static AccountType toAccountType(String value) {
if (value == null) {
throw new FQLException("NULL cannot be solved to AccountType!");
}
try {
return AccountType.valueOf(value.toUpperCase());
}
catch (IllegalArgumentException e) {
throw new FQLException(e);
}
}
}

View File

@@ -0,0 +1,24 @@
package de.financer.fql.field_handler;
import de.financer.fql.FQLException;
import de.financer.fql.FieldMapping;
import de.financer.fql.join_handler.JoinKey;
import org.apache.commons.lang3.math.NumberUtils;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Predicate;
import java.util.Map;
public class PeriodIdHandler implements FieldHandler<String> {
@Override
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> froms, CriteriaBuilder criteriaBuilder, String value) {
final String unquotedValue = FieldHandlerUtils.removeQuotes(value);
if (!NumberUtils.isCreatable(unquotedValue)) {
throw new FQLException(String.format("Invalid value %s for field periodId - only numbers allowed!", unquotedValue));
}
return criteriaBuilder.equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), NumberUtils.toLong(unquotedValue));
}
}

View File

@@ -58,31 +58,72 @@ public class PeriodController {
return "period/periodOverview";
}
@GetMapping("/showTransactions")
public String showTransactions(Model model, Long periodId) {
private String showInternal(Model model, Long periodId, String fql, boolean showSum) {
final SearchTransactionsForm form = new SearchTransactionsForm();
form.setFql(fql);
try {
final Iterable<SearchTransactionsResponseDto> response =
new SearchTransactionsTemplate()
.exchangeGet(financerConfig, null, null, periodId, null,
Order.TRANSACTIONS_BY_DATE_DESC, null, false);
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_SEARCH_BY_FQL));
final List<SearchTransactionsResponseDto> transactions = IterableUtils.toList(response);
transactionBuilder.queryParam("fql", form.getFql());
final Iterable<SearchTransactionsResponseDto> trxs = FinancerRestTemplate.exchangeGet(transactionBuilder,
new ParameterizedTypeReference<Iterable<SearchTransactionsResponseDto>>() {
});
model.addAttribute("transactions", transactions);
model.addAttribute("transactionCount", IterableUtils.size(transactions));
model.addAttribute("transactions", trxs);
model.addAttribute("transactionCount", IterableUtils.size(trxs));
if (showSum) {
Long trxSum = IterableUtils.toList(trxs).stream().mapToLong(t -> t.getAmount()).sum();
model.addAttribute("transactionSum", trxSum);
}
catch(FinancerRestException e) {
} catch (FinancerRestException e) {
model.addAttribute("errorMessage", e.getResponseReason().name());
model.addAttribute("transactionCount", 0);
}
model.addAttribute("form", new SearchTransactionsForm());
model.addAttribute("showSum", false);
model.addAttribute("form", form);
model.addAttribute("showSum", showSum);
model.addAttribute("returnTo", "/periodOverview");
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
ControllerUtils.addDarkMode(model, this.financerConfig);
return "transaction/searchTransactions";
}
@GetMapping("/showAllTransactions")
public String showAllTransactions(Model model, Long periodId) {
final String fql = String.format("periodId = '%s' ORDER BY date DESC", periodId);
return showInternal(model, periodId, fql, false);
}
@GetMapping("/showIncomeTransactions")
public String showIncomeTransactions(Model model, Long periodId) {
final String fql = String
.format("periodId = '%s' AND (fromAccountType = 'INCOME' OR (fromAccountType = 'LIABILITY' AND (toAccountType = 'BANK' OR toAccountType = 'CASH')) OR (fromAccountType = 'START' AND (toAccountType = 'BANK' OR toAccountType = 'CASH'))) ORDER BY date DESC", periodId);
return showInternal(model, periodId, fql, true);
}
@GetMapping("/showExpenseTransactions")
public String showExpenseTransactions(Model model, Long periodId) {
final String fql = String
.format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'EXPENSE' ORDER BY date DESC", periodId);
return showInternal(model, periodId, fql, true);
}
@GetMapping("/showLiabilityTransactions")
public String showLiabilityTransactions(Model model, Long periodId) {
final String fql = String
.format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'LIABILITY' ORDER BY date DESC", periodId);
return showInternal(model, periodId, fql, true);
}
}

View File

@@ -138,6 +138,8 @@ financer.search-transactions.show-query-options.fromAccount=fromAccount\: the ke
financer.search-transactions.show-query-options.toAccount=toAccount\: the key of the to account
financer.search-transactions.show-query-options.fromAccountGroup=fromAccountGroup\: the name of the account group of the from account
financer.search-transactions.show-query-options.toAccountGroup=toAccountGroup\: the name of the account group of the to account
financer.search-transactions.show-query-options.fromAccountType=fromAccountType\: the type of the from account
financer.search-transactions.show-query-options.toAccountType=toAccountType\: the type of the to account
financer.search-transactions.show-query-options.date=date\: the date of the transaction in the format yyyy-mm-dd
financer.search-transactions.show-query-options.recurring=recurring\: whether the transaction has been created from a recurring transaction
financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: whether the transaction is relevant for tax declaration
@@ -185,7 +187,11 @@ financer.period-overview.table-header.total=Total
financer.period-overview.table-header.assets=Assets
financer.period-overview.table-header.transactions=Transaction count
financer.period-overview.table-header.actions=Actions
financer.period-overview.table.actions.showTransactions=Show transactions
financer.period-overview.table.actions.showAllTransactions=All transactions
financer.period-overview.table.actions.showIncomeTransactions=Income transactions
financer.period-overview.table.actions.showExpenseTransactions=Expense transactions
financer.period-overview.table.actions.showLiabilityTransactions=Liability transactions
financer.period-overview.show-actions=Show...
financer.interval-type.DAILY=Daily
financer.interval-type.WEEKLY=Weekly

View File

@@ -138,6 +138,8 @@ financer.search-transactions.show-query-options.fromAccount=fromAccount\: der Sc
financer.search-transactions.show-query-options.toAccount=toAccount\: der Schl\u00FCssel des An Kontos
financer.search-transactions.show-query-options.fromAccountGroup=fromAccountGroup\: der Name der Kontogruppe des Von Kontos
financer.search-transactions.show-query-options.toAccountGroup=toAccountGroup\: der Name der Kontogruppe des An Kontos
financer.search-transactions.show-query-options.fromAccountType=fromAccountType\: der Typ des Von Kontos
financer.search-transactions.show-query-options.toAccountType=toAccountType\: der Typ des An Kontos
financer.search-transactions.show-query-options.date=date\: das Datum der Buchung im Format jjjj-mm-tt
financer.search-transactions.show-query-options.recurring=recurring\: ob die Buchung durch eine wiederkehrende Buchung erzeugt wurde
financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: ob die Buchung als steuerrelevant markiert wurde
@@ -185,7 +187,11 @@ financer.period-overview.table-header.total=Insgesamt
financer.period-overview.table-header.assets=Umlaufverm\u00F6gen
financer.period-overview.table-header.transactions=Anzahl Buchungen
financer.period-overview.table-header.actions=Aktionen
financer.period-overview.table.actions.showTransactions=Zeige Buchungen
financer.period-overview.table.actions.showAllTransactions=Alle Buchungen
financer.period-overview.table.actions.showIncomeTransactions=Einkommenbuchungen
financer.period-overview.table.actions.showExpenseTransactions=Ausgabebuchungen
financer.period-overview.table.actions.showLiabilityTransactions=Verbindlichkeitenbuchungen
financer.period-overview.show-actions=Anzeigen...
financer.interval-type.DAILY=T\u00E4glich
financer.interval-type.WEEKLY=W\u00F6chentlich

View File

@@ -1,5 +1,7 @@
v45 -> v46:
- #17 Add actions to the period overview
- #18 Improve actions in the period overview
- Introduce new FQL fields toAccountType and fromAccountType
v44 -> v45:
- #5 Having no periods breaks the "expenses per period" graph on the account overview page

View File

@@ -46,6 +46,14 @@
color: var(--neutral-color);
}
.expense-period-period-overview {
padding-left: 1em;
}
.expense-year-period-period-overview {
font-weight: bolder;
}
#period-overview-asset-container {
display: inline;
}

View File

@@ -12,7 +12,7 @@
<body>
<h1 th:text="#{financer.heading.period-overview}"/>
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<a th:href="@{/accountOverview}" th:text="#{financer.cancel-back-to-overview}"/>
<a th:href="@{/accountOverview}" th:text="#{financer.back-to-overview}"/>
<table id="period-overview-table">
<tr>
<th th:text="#{financer.period-overview.table-header.id}"/>
@@ -28,30 +28,62 @@
<th th:text="#{financer.period-overview.table-header.actions}"/>
</tr>
<tr th:each="periodOverview : ${periodOverviews}">
<td th:text="${periodOverview.periodId}"/>
<td th:text="#{'financer.period-type.' + ${periodOverview.periodType}}"/>
<td th:text="${#temporals.format(periodOverview.periodStart)}"/>
<td th:text="${#temporals.format(periodOverview.periodEnd)}"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.incomeSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.expenseSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.liabilitySum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:text="${periodOverview.periodId}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td>
<div>
<span th:text="#{'financer.period-type.' + ${periodOverview.periodType}}"
th:classappend="${(periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE ? 'expense-period-period-overview' : '') +
(periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR ? 'expense-year-period-period-overview' : '')}"/>
</div>
</td>
<td th:text="${#temporals.format(periodOverview.periodStart)}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td th:text="${#temporals.format(periodOverview.periodEnd)}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.incomeSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.expenseSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.liabilitySum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td th:utext="${#numbers.formatDecimal(periodOverview.total/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"
th:classappend="${periodOverview.total > periodOverview.incomeSum} ? overspend"/>
th:classappend="${(periodOverview.total > periodOverview.incomeSum ? 'overspend' : '') +
(periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR ? ' expense-year-period-period-overview' : '')}"/>
<td th:if="${periodOverview.assetsSum != null}">
<div id="period-overview-asset-container">
<span th:utext="${#numbers.formatDecimal(periodOverview.assetsSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<span th:utext="${#numbers.formatDecimal(periodOverview.assetsSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<span th:if="${periodOverview.assetTrend == T(de.financer.dto.AssetTrend).UP}" class="icon color-good">&#xe8e5;</span>
<span th:if="${periodOverview.assetTrend == T(de.financer.dto.AssetTrend).DOWN}" class="icon color-bad">&#xe8e3;</span>
<span th:if="${periodOverview.assetTrend == T(de.financer.dto.AssetTrend).EQUAL}" class="icon color-neutral">&#xe8e4;</span>
</div>
</td>
<td th:if="${periodOverview.assetsSum == null}" />
<td th:text="${periodOverview.transactionCount}"/>
<td th:text="${periodOverview.transactionCount}"
th:classappend="${periodOverview.periodType == T(de.financer.model.PeriodType).EXPENSE_YEAR} ? expense-year-period-period-overview"/>
<td nowrap>
<div id="period-overview-actions-container">
<a th:href="@{/showTransactions(periodId=${periodOverview.periodId})}"
th:text="#{financer.period-overview.table.actions.showTransactions}" />
<details>
<summary th:text="#{financer.period-overview.show-actions}"/>
<div id="period-overview-show-actions-detail-container">
<div id="period-overview-show-actions-detail-container-show-all">
<a th:href="@{/showAllTransactions(periodId=${periodOverview.periodId})}"
th:text="#{financer.period-overview.table.actions.showAllTransactions}" />
</div>
<div id="period-overview-show-actions-detail-container-show-income">
<a th:href="@{/showIncomeTransactions(periodId=${periodOverview.periodId})}"
th:text="#{financer.period-overview.table.actions.showIncomeTransactions}" />
</div>
<div id="period-overview-show-actions-detail-container-show-expense">
<a th:href="@{/showExpenseTransactions(periodId=${periodOverview.periodId})}"
th:text="#{financer.period-overview.table.actions.showExpenseTransactions}" />
</div>
<div id="period-overview-show-actions-detail-container-show-liability">
<a th:href="@{/showLiabilityTransactions(periodId=${periodOverview.periodId})}"
th:text="#{financer.period-overview.table.actions.showLiabilityTransactions}" />
</div>
</div>
</details>
</td>
</tr>
</table>

View File

@@ -12,7 +12,8 @@
<body>
<h1 th:text="#{financer.heading.search-transactions}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<a th:href="@{/accountOverview}" th:text="#{financer.search-transactions.back-to-overview}"/>
<a th:if="${returnTo == null}" th:href="@{/accountOverview}" th:text="#{financer.search-transactions.back-to-overview}"/>
<a th:if="${returnTo != null}" th:href="@{${returnTo}}" th:text="#{financer.search-transactions.back-to-overview}"/>
<form id="search-transactions-form" action="#" th:action="@{/searchTransactions}" th:object="${form}"
method="post">
<label for="inputFql" th:text="#{financer.search-transactions.label.fql}"/>
@@ -30,6 +31,8 @@
<li th:text="#{financer.search-transactions.show-query-options.toAccount}" />
<li th:text="#{financer.search-transactions.show-query-options.fromAccountGroup}" />
<li th:text="#{financer.search-transactions.show-query-options.toAccountGroup}" />
<li th:text="#{financer.search-transactions.show-query-options.fromAccountType}" />
<li th:text="#{financer.search-transactions.show-query-options.toAccountType}" />
<li th:text="#{financer.search-transactions.show-query-options.date}" />
<li th:text="#{financer.search-transactions.show-query-options.recurring}" />
<li th:text="#{financer.search-transactions.show-query-options.taxRelevant}" />