Add expense history to overview page
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.financer.chart.impl;
|
||||
|
||||
import de.financer.chart.ChartParameter;
|
||||
|
||||
public class EmptyParameter implements ChartParameter {
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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\:
|
||||
|
||||
@@ -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\:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}"/>
|
||||
|
||||
Reference in New Issue
Block a user