Add expense history to overview page

This commit is contained in:
2019-10-08 22:41:11 +02:00
parent 1fb4c8fc98
commit b81303f20d
20 changed files with 217 additions and 30 deletions

View File

@@ -11,6 +11,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("transactions")
public class TransactionController {
@@ -108,4 +110,19 @@ public class TransactionController {
return expensePeriodTotals;
}
@RequestMapping("getExpensesAllPeriods")
public List<Long> getExpensesAllPeriods() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesAllPeriods called"));
}
final List<Long> response = this.transactionService.getExpensesAllPeriods();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesAllPeriods returns with %s", response));
}
return response;
}
}

View File

@@ -20,6 +20,9 @@ public interface TransactionRepository extends CrudRepository<Transaction, Long>
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period")
Long getExpensesForPeriod(Period period, AccountType... expenseTypes);
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes GROUP BY p ORDER BY p.start ASC")
List<Long> getExpensesForAllPeriods(AccountType... expenseTypes);
// The HQL contains a hack because Hibernate can't resolve the alias of the CASE column in the GROUP BY clause
// That's why the generated alias is used directly in the HQL. It will break if the columns in the SELECT clause get reordered
// col_2_0_ instead of AccType

View File

@@ -18,6 +18,7 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Service
@@ -243,6 +244,10 @@ public class TransactionService {
return Optional.ofNullable(expensesCurrentPeriod).orElse(Long.valueOf(0l));
}
public List<Long> getExpensesAllPeriods() {
return this.transactionRepository.getExpensesForAllPeriods(AccountType.EXPENSE, AccountType.LIABILITY);
}
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
final Iterable<Period> periods = this.periodService.getAllExpensePeriodsForYear(year);

View File

@@ -0,0 +1,17 @@
package de.financer.chart;
import java.awt.*;
public class ChartDefinitions {
public static final Color TRANSPARENT = new Color(0.0f, 0.0f, 0.0f, 0.0f);
public static final Color BLUE = new Color(0, 143, 251);
public static final Color RED = new Color(211, 0, 0);
public static final Color GREEN = new Color(0, 169, 0);
public static final Color CYAN = new Color(0, 127, 127);
public static final Color ORANGE = new Color(211, 96, 0);
public static final Color PURPLE = new Color(171, 0, 93);
public static final Color PINK = new Color(253, 0, 153);
public static final Color MINT = new Color(0, 194, 78);
public static final Color YELLOW = new Color(255, 203, 0);
}

View File

@@ -5,14 +5,22 @@ import java.util.List;
import java.util.stream.Collectors;
public enum ChartType {
ACCOUNT_GROUP_EXPENSES_CURRENT_PERIOD,
ACCOUNT_GROUP_EXPENSES_FOR_PERIOD,
ACCOUNT_EXPENSES_CURRENT_PERIOD,
ACCOUNT_EXPENSES_FOR_PERIOD,
EXPENSE_PERIOD_TOTALS_CURRENT_YEAR;
ACCOUNT_GROUP_EXPENSES_CURRENT_PERIOD(false),
ACCOUNT_GROUP_EXPENSES_FOR_PERIOD(false),
ACCOUNT_EXPENSES_CURRENT_PERIOD(false),
ACCOUNT_EXPENSES_FOR_PERIOD(false),
EXPENSE_PERIOD_TOTALS_CURRENT_YEAR(false),
EXPENSES_ALL_PERIODS_INLINE(true);
public static List<String> valueList() {
return Arrays.stream(ChartType.values()).map(ChartType::name).collect(Collectors.toList());
private boolean inline;
ChartType(boolean inline) {
this.inline = inline;
}
public static List<String> valueList(boolean filterInline) {
return Arrays.stream(ChartType.values()).filter((ct) -> filterInline ? !ct.inline : true).map(ChartType::name)
.collect(Collectors.toList());
}
public static ChartType getByValue(String value) {

View File

@@ -2,6 +2,7 @@ package de.financer.chart;
import de.financer.chart.impl.expense.AccountExpensesGenerator;
import de.financer.chart.impl.expense.AccountGroupExpensesGenerator;
import de.financer.chart.impl.inline.ExpensesAllPeriodsGenerator;
import de.financer.chart.impl.total.PeriodTotalGenerator;
import de.financer.config.FinancerConfig;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +37,10 @@ public class FinancerChartFactory {
// TODO WHY IS THIS CAST NECESSARY???
generator = (AbstractChartGenerator<P>) new PeriodTotalGenerator();
break;
case EXPENSES_ALL_PERIODS_INLINE:
// TODO WHY IS THIS CAST NECESSARY???
generator = (AbstractChartGenerator<P>) new ExpensesAllPeriodsGenerator();
break;
default:
generator = null;
}

View File

@@ -0,0 +1,6 @@
package de.financer.chart.impl;
import de.financer.chart.ChartParameter;
public class EmptyParameter implements ChartParameter {
}

View File

@@ -1,6 +1,7 @@
package de.financer.chart.impl.expense;
import de.financer.chart.AbstractChartGenerator;
import de.financer.chart.ChartDefinitions;
import de.financer.config.FinancerConfig;
import de.financer.util.ControllerUtils;
import org.jfree.chart.ChartFactory;
@@ -23,6 +24,8 @@ public abstract class AbstractExpensesGenerator extends AbstractChartGenerator<E
final JFreeChart chart = ChartFactory
.createPieChart(this.getMessage(parameter.getTitle(), parameter.getArgsForTitle()), dataSet);
chart.getCategoryPlot().setBackgroundPaint(ChartDefinitions.TRANSPARENT);
final NumberFormat currencyInstance = NumberFormat.getCurrencyInstance(LocaleContextHolder.getLocale());
currencyInstance.setCurrency(this.getFinancerConfig().getCurrency());

View File

@@ -0,0 +1,46 @@
package de.financer.chart.impl.inline;
import de.financer.chart.AbstractChartGenerator;
import de.financer.chart.ChartDefinitions;
import de.financer.chart.impl.EmptyParameter;
import de.financer.template.GetExpensesAllPeriodsTemplate;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import java.awt.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class ExpensesAllPeriodsGenerator extends AbstractChartGenerator<EmptyParameter> {
@Override
public JFreeChart generateChart(EmptyParameter parameter) {
final CategoryDataset dataSet = getDataset();
final JFreeChart chart = ChartFactory
.createLineChart("", "", "", dataSet, PlotOrientation.VERTICAL, false, false, false);
chart.getCategoryPlot().getRangeAxis().setVisible(false);
chart.getCategoryPlot().getDomainAxis().setVisible(false);
chart.getCategoryPlot().setOutlineVisible(false);
chart.getCategoryPlot().setBackgroundPaint(ChartDefinitions.TRANSPARENT);
chart.getCategoryPlot().getRenderer().setSeriesPaint(0, ChartDefinitions.BLUE);
chart.getCategoryPlot().getRenderer().setSeriesStroke(0, new BasicStroke(2f));
return chart;
}
private CategoryDataset getDataset() {
final DefaultCategoryDataset result = new DefaultCategoryDataset();
final List<Long> totalData = new GetExpensesAllPeriodsTemplate().exchange(this.getFinancerConfig()).getBody();
final AtomicInteger counter = new AtomicInteger();
totalData.stream().forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet()));
return result;
}
}

View File

@@ -1,6 +1,7 @@
package de.financer.chart.impl.total;
import de.financer.chart.AbstractChartGenerator;
import de.financer.chart.ChartDefinitions;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.model.Period;
import de.financer.template.GetExpensePeriodTotalsTemplate;
@@ -29,6 +30,11 @@ public class PeriodTotalGenerator extends AbstractChartGenerator<PeriodTotalPara
this.getMessage(parameter.getxAxis()),
dataSet, PlotOrientation.VERTICAL, true, true, false);
chart.getCategoryPlot().getRenderer().setSeriesPaint(0, ChartDefinitions.RED);
chart.getCategoryPlot().getRenderer().setSeriesPaint(1, ChartDefinitions.GREEN);
chart.getCategoryPlot().getRenderer().setSeriesPaint(2, ChartDefinitions.BLUE);
chart.getCategoryPlot().setBackgroundPaint(ChartDefinitions.TRANSPARENT);
final NumberAxis axis = (NumberAxis) chart.getCategoryPlot().getRangeAxis();
axis.setStandardTickUnits(NumberAxis.createIntegerTickUnits(LocaleContextHolder

View File

@@ -3,6 +3,7 @@ package de.financer.controller;
import de.financer.chart.ChartGenerator;
import de.financer.chart.ChartType;
import de.financer.chart.FinancerChartFactory;
import de.financer.chart.impl.EmptyParameter;
import de.financer.chart.impl.expense.ExpensesParameter;
import de.financer.chart.impl.total.PeriodTotalParameter;
import de.financer.config.FinancerConfig;
@@ -13,6 +14,9 @@ import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -123,4 +127,21 @@ public class ChartController {
// TODO
}
}
@GetMapping("/getExpensesAllPeriodsInline")
public ResponseEntity<ByteArrayResource> getExpensesAllPeriodsInline() {
final ChartGenerator<EmptyParameter> generator =
this.financerChartFactory.getGenerator(ChartType.EXPENSES_ALL_PERIODS_INLINE);
final JFreeChart chart = generator.generateChart(new EmptyParameter());
try {
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.body(new ByteArrayResource(ChartUtils
.encodeAsPNG(chart.createBufferedImage(400, 80))));
}
catch (IOException e) {
return null;
}
}
}

View File

@@ -21,6 +21,7 @@ public enum Function {
TR_DELETE_TRANSACTION("transactions/deleteTransaction"),
TR_EXPENSES_CURRENT_PERIOD("transactions/getExpensesCurrentPeriod"),
TR_EXPENSE_PERIOD_TOTALS("transactions/getExpensePeriodTotals"),
TR_EXPENSES_ALL_PERIODS("transactions/getExpensesAllPeriods"),
RT_GET_ALL("recurringTransactions/getAll"),
RT_GET_ALL_ACTIVE("recurringTransactions/getAllActive"),

View File

@@ -22,7 +22,7 @@ public class ReportController {
@GetMapping("/selectChart")
public String selectChart(Model model) {
model.addAttribute("form", new SelectChartForm());
model.addAttribute("availableCharts", ChartType.valueList());
model.addAttribute("availableCharts", ChartType.valueList(true));
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -39,7 +39,7 @@ public class ReportController {
catch (NoSuchElementException nsee) {
model.addAttribute("form", new SelectChartForm());
model.addAttribute("errorMessage", "UNKNOWN_CHART_TYPE");
model.addAttribute("availableCharts", ChartType.valueList());
model.addAttribute("availableCharts", ChartType.valueList(true));
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);

View File

@@ -0,0 +1,22 @@
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;
public class GetExpensesAllPeriodsTemplate {
public ResponseEntity<List<Long>> exchange(FinancerConfig financerConfig) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSES_ALL_PERIODS));
return new FinancerRestTemplate<List<Long>>()
.exchange(builder.toUriString(), new ParameterizedTypeReference<List<Long>>() {
});
}
}

View File

@@ -24,6 +24,7 @@ financer.account-overview.table-header.type=Type
financer.account-overview.table-header.status=Status
financer.account-overview.tooltip.status.current-expenses=Period starting at {0}. Clicking the amount will open a graphical overview about the expenses grouped by account group
financer.account-overview.tooltip.status.current-assets=Assets available at short-notice
financer.account-overview.expense-history.description=Expense history
financer.account-new.title=financer\: create new account
financer.account-new.label.key=Key\:

View File

@@ -22,6 +22,7 @@ financer.account-overview.table-header.type=Typ
financer.account-overview.table-header.status=Status
financer.account-overview.tooltip.status.current-expenses=Periode ab {0}. Durch Klicken des Betrags kann eine grafische \u00DCbersicht über die Ausgaben gruppiert nach Konto-Gruppe angezeigt werden
financer.account-overview.tooltip.status.current-assets=Kurzfristig verf\u00FCgbares Verm\u00F6gen
financer.account-overview.expense-history.description=Ausgabenhistorie
financer.account-new.title=financer\: Neues Konto erstellen
financer.account-new.label.key=Schl\u00FCssel\:

View File

@@ -1,6 +1,7 @@
v23 -> v24:
- Mark accounts that are overspend, meaning their spending in the current period is greater than the average spending of
that account
- Add expense history to overview page
v22 -> v23:
- Make table headers sticky if the table scrolls

View File

@@ -1,3 +1,9 @@
/* Variable declarations */
:root {
--error-color: #D30000/*#ff6666*/;
}
/* --------------------- */
#account-overview-table,
#account-transaction-table,
#recurring-transaction-list-table {
@@ -32,7 +38,7 @@ tr:hover {
}
.overspend {
color: #ff6666;
color: var(--error-color);
}
@media only screen and (max-width: 450px) {
@@ -65,6 +71,19 @@ tr:hover {
#details-container,
#status-container {
margin-bottom: 1em;
padding-inline-end: 1em;
}
#header-container > div {
display: inline-block;
}
#expense-history-container {
float: right;
}
#expense-history-description {
text-align: center;
}
#status-container > span, div {
@@ -118,6 +137,6 @@ input[type=submit] {
}
.errorMessage {
color: #ff6666;
color: var(--error-color);
display: block;
}

View File

@@ -149,7 +149,6 @@
11. Planned features
====================
This chapter lists planned features. The list is in no particular order:
- Budgeting
- Transaction import from online banking (file based)
- Extended reports, e.g. forecasting based on recurring transactions and average spending
- Receivable account type

View File

@@ -9,25 +9,31 @@
<body>
<h1 th:text="#{financer.heading.account-overview}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<div id="status-container">
<span th:text="#{financer.account-overview.status}"/>
<div th:title="#{financer.account-overview.tooltip.status.current-assets}">
<span th:text="#{financer.account-overview.status.current-assets}"/>
<span th:text="${#numbers.formatDecimal(currentAssets/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<div id="header-container">
<div id="status-container">
<span th:text="#{financer.account-overview.status}"/>
<div th:title="#{financer.account-overview.tooltip.status.current-assets}">
<span th:text="#{financer.account-overview.status.current-assets}"/>
<span th:text="${#numbers.formatDecimal(currentAssets/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
</div>
<div th:title="#{'financer.account-overview.tooltip.status.current-expenses'(${#temporals.format(periodStart)})}">
<span th:text="#{financer.account-overview.status.current-expenses}"/>
<a th:href="@{/getAccountGroupExpensesCurrentPeriod}">
<span th:text="${#numbers.formatDecimal(currentExpenses/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}" />
</a>
</div>
<div>
<span th:text="#{financer.account-overview.status.recurring-transaction-due-today}"/>
<a th:href="@{/recurringTransactionDueToday}" th:text="${rtDueTodayCount}"/>
</div>
<div>
<span th:text="#{financer.account-overview.status.recurring-transaction-active}"/>
<a th:href="@{/recurringTransactionActive}" th:text="${rtAllActiveCount}"/>
</div>
</div>
<div th:title="#{'financer.account-overview.tooltip.status.current-expenses'(${#temporals.format(periodStart)})}">
<span th:text="#{financer.account-overview.status.current-expenses}"/>
<a th:href="@{/getAccountGroupExpensesCurrentPeriod}">
<span th:text="${#numbers.formatDecimal(currentExpenses/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}" />
</a>
</div>
<div>
<span th:text="#{financer.account-overview.status.recurring-transaction-due-today}"/>
<a th:href="@{/recurringTransactionDueToday}" th:text="${rtDueTodayCount}"/>
</div>
<div>
<span th:text="#{financer.account-overview.status.recurring-transaction-active}"/>
<a th:href="@{/recurringTransactionActive}" th:text="${rtAllActiveCount}"/>
<div id="expense-history-container">
<div id="expense-history-description" th:text="#{financer.account-overview.expense-history.description}" />
<img class="picture" th:src="@{'/getExpensesAllPeriodsInline'}" />
</div>
</div>
<span th:text="#{financer.account-overview.available-actions}"/>