From 5a13429e9b6ae0a53d5f223aeb2e0249d8c7bf38 Mon Sep 17 00:00:00 2001 From: MK13 Date: Thu, 4 Mar 2021 18:12:18 +0100 Subject: [PATCH] #10 Create period overview WIP commit --- .../de/financer/dto/PeriodOverviewDto.java | 130 ++++++++++++++++++ .../main/java/de/financer/model/Period.java | 20 +++ .../financer/controller/PeriodController.java | 18 +++ .../java/de/financer/dba/NativeQueries.java | 22 +++ .../de/financer/dba/PeriodRepository.java | 2 +- .../financer/dba/PeriodRepositoryCustom.java | 9 ++ .../dba/impl/PeriodRepositoryCustomImpl.java | 23 ++++ .../de/financer/service/PeriodService.java | 6 + .../resources/native_queries/folder-info.txt | 10 ++ .../native_queries/period_overview.sql | 130 ++++++++++++++++++ ...ller_getPeriodOverviewIntegrationTest.java | 50 +++++++ .../java/de/financer/controller/Function.java | 1 + .../financer/controller/PeriodController.java | 31 +++++ .../main/resources/i18n/message.properties | 18 +++ .../resources/i18n/message_de_DE.properties | 18 +++ .../src/main/resources/static/css/main.css | 10 +- .../templates/account/accountOverview.html | 2 + .../templates/period/periodOverview.html | 47 +++++++ 18 files changed, 543 insertions(+), 4 deletions(-) create mode 100644 financer-common/src/main/java/de/financer/dto/PeriodOverviewDto.java create mode 100644 financer-server/src/main/java/de/financer/dba/NativeQueries.java create mode 100644 financer-server/src/main/java/de/financer/dba/PeriodRepositoryCustom.java create mode 100644 financer-server/src/main/java/de/financer/dba/impl/PeriodRepositoryCustomImpl.java create mode 100644 financer-server/src/main/resources/native_queries/folder-info.txt create mode 100644 financer-server/src/main/resources/native_queries/period_overview.sql create mode 100644 financer-server/src/test/java/de/financer/controller/integration/PeriodController_getPeriodOverviewIntegrationTest.java create mode 100644 financer-web-client/src/main/resources/templates/period/periodOverview.html diff --git a/financer-common/src/main/java/de/financer/dto/PeriodOverviewDto.java b/financer-common/src/main/java/de/financer/dto/PeriodOverviewDto.java new file mode 100644 index 0000000..1a0e986 --- /dev/null +++ b/financer-common/src/main/java/de/financer/dto/PeriodOverviewDto.java @@ -0,0 +1,130 @@ +package de.financer.dto; + +import de.financer.model.PeriodType; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; + +import java.time.LocalDate; + +public class PeriodOverviewDto { + private Long periodId; + private PeriodType periodType; + private LocalDate periodStart; + private LocalDate periodEnd; + private Long incomeSum; + private Long expenseSum; + private Long liabilitySum; + private Long total; + private Long assetsSum; + private Long transactionCount; + + public PeriodOverviewDto() { + + } + + public PeriodOverviewDto(Long periodId, + String periodType, + LocalDate periodStart, + LocalDate periodEnd, + Long incomeSum, + Long expenseSum, + Long liabilitySum, + Long total, + Long assetsSum, + Long transactionCount) { + this.periodId = periodId; + this.periodType = PeriodType.valueOf(periodType); + this.periodStart = periodStart; + this.periodEnd = periodEnd; + this.incomeSum = incomeSum; + this.expenseSum = expenseSum; + this.liabilitySum = liabilitySum; + this.total = total; + this.assetsSum = assetsSum; + this.transactionCount = transactionCount; + } + + public Long getPeriodId() { + return periodId; + } + + public void setPeriodId(Long periodId) { + this.periodId = periodId; + } + + public PeriodType getPeriodType() { + return periodType; + } + + public void setPeriodType(PeriodType periodType) { + this.periodType = periodType; + } + + public LocalDate getPeriodStart() { + return periodStart; + } + + public void setPeriodStart(LocalDate periodStart) { + this.periodStart = periodStart; + } + + public LocalDate getPeriodEnd() { + return periodEnd; + } + + public void setPeriodEnd(LocalDate periodEnd) { + this.periodEnd = periodEnd; + } + + public Long getIncomeSum() { + return incomeSum; + } + + public void setIncomeSum(Long incomeSum) { + this.incomeSum = incomeSum; + } + + public Long getExpenseSum() { + return expenseSum; + } + + public void setExpenseSum(Long expenseSum) { + this.expenseSum = expenseSum; + } + + public Long getLiabilitySum() { + return liabilitySum; + } + + public void setLiabilitySum(Long liabilitySum) { + this.liabilitySum = liabilitySum; + } + + public Long getTotal() { + return total; + } + + public void setTotal(Long total) { + this.total = total; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } + + public Long getAssetsSum() { + return assetsSum; + } + + public void setAssetsSum(Long assetsSum) { + this.assetsSum = assetsSum; + } + + public Long getTransactionCount() { + return transactionCount; + } + + public void setTransactionCount(Long transactionCount) { + this.transactionCount = transactionCount; + } +} diff --git a/financer-common/src/main/java/de/financer/model/Period.java b/financer-common/src/main/java/de/financer/model/Period.java index 6833d6c..e1b6e09 100644 --- a/financer-common/src/main/java/de/financer/model/Period.java +++ b/financer-common/src/main/java/de/financer/model/Period.java @@ -1,9 +1,29 @@ package de.financer.model; +import de.financer.dto.PeriodOverviewDto; + import javax.persistence.*; +import java.time.LocalDate; import java.time.LocalDateTime; @Entity +// I don't like having that here, but it needs to be annotated on some @Entity +// and this entity is the best match +@SqlResultSetMapping(name = "PeriodOverviewResult", + classes = { + @ConstructorResult(targetClass = PeriodOverviewDto.class, + columns = { + @ColumnResult(name = "PERIOD_ID", type = Long.class), + @ColumnResult(name = "PERIOD_TYPE", type = String.class), + @ColumnResult(name = "PERIOD_START", type = LocalDate.class), + @ColumnResult(name = "PERIOD_END", type = LocalDate.class), + @ColumnResult(name = "INCOME_SUM", type = Long.class), + @ColumnResult(name = "EXPENSE_SUM", type = Long.class), + @ColumnResult(name = "LIABILITY_SUM", type = Long.class), + @ColumnResult(name = "TOTAL", type = Long.class), + @ColumnResult(name = "ASSETS_SUM", type = Long.class), + @ColumnResult(name = "TRANSACTION_COUNT", type = Long.class)}) + }) public class Period { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/financer-server/src/main/java/de/financer/controller/PeriodController.java b/financer-server/src/main/java/de/financer/controller/PeriodController.java index 1cf6b1f..d652126 100644 --- a/financer-server/src/main/java/de/financer/controller/PeriodController.java +++ b/financer-server/src/main/java/de/financer/controller/PeriodController.java @@ -1,5 +1,6 @@ package de.financer.controller; +import de.financer.dto.PeriodOverviewDto; import de.financer.model.Period; import de.financer.service.PeriodService; import org.slf4j.Logger; @@ -9,6 +10,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("periods") public class PeriodController { @@ -46,4 +49,19 @@ public class PeriodController { return currentExpensePeriod; } + + @RequestMapping("getPeriodOverview") + public List getPeriodOverview() { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("/periods/getPeriodOverview called"); + } + + final List overview = this.periodService.getPeriodOverview(); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("/periods/getPeriodOverview returns with %s", overview)); + } + + return overview; + } } diff --git a/financer-server/src/main/java/de/financer/dba/NativeQueries.java b/financer-server/src/main/java/de/financer/dba/NativeQueries.java new file mode 100644 index 0000000..bfa7cd1 --- /dev/null +++ b/financer-server/src/main/java/de/financer/dba/NativeQueries.java @@ -0,0 +1,22 @@ +package de.financer.dba; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +public enum NativeQueries { + PERIOD_OVERVIEW_QUERY("period_overview.sql"); + + private String query; + + private NativeQueries(String fileName) { + this.query = new BufferedReader( + new InputStreamReader(NativeQueries.class.getClassLoader() + .getResourceAsStream("native_queries/" + fileName))) + .lines().collect(Collectors.joining("\n")); + } + + public String getQuery() { + return this.query; + } +} diff --git a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java index db8a872..0e7ac5e 100644 --- a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java +++ b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java @@ -10,7 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; @Transactional(propagation = Propagation.REQUIRED) -public interface PeriodRepository extends CrudRepository { +public interface PeriodRepository extends CrudRepository, PeriodRepositoryCustom { @Query("SELECT p FROM Period p WHERE p.type = :type AND p.end IS NULL") Period findCurrentExpensePeriod(PeriodType type); diff --git a/financer-server/src/main/java/de/financer/dba/PeriodRepositoryCustom.java b/financer-server/src/main/java/de/financer/dba/PeriodRepositoryCustom.java new file mode 100644 index 0000000..ea41c91 --- /dev/null +++ b/financer-server/src/main/java/de/financer/dba/PeriodRepositoryCustom.java @@ -0,0 +1,9 @@ +package de.financer.dba; + +import de.financer.dto.PeriodOverviewDto; + +import java.util.List; + +public interface PeriodRepositoryCustom { + List getPeriodOverview(); +} diff --git a/financer-server/src/main/java/de/financer/dba/impl/PeriodRepositoryCustomImpl.java b/financer-server/src/main/java/de/financer/dba/impl/PeriodRepositoryCustomImpl.java new file mode 100644 index 0000000..182fa4a --- /dev/null +++ b/financer-server/src/main/java/de/financer/dba/impl/PeriodRepositoryCustomImpl.java @@ -0,0 +1,23 @@ +package de.financer.dba.impl; + +import de.financer.dba.NativeQueries; +import de.financer.dba.PeriodRepositoryCustom; +import de.financer.dto.PeriodOverviewDto; +import org.springframework.stereotype.Repository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; + +@Repository +public class PeriodRepositoryCustomImpl implements PeriodRepositoryCustom { + @PersistenceContext + private EntityManager entityManager; + + @Override + public List getPeriodOverview() { + return this.entityManager + .createNativeQuery(NativeQueries.PERIOD_OVERVIEW_QUERY.getQuery(), "PeriodOverviewResult") + .getResultList(); + } +} diff --git a/financer-server/src/main/java/de/financer/service/PeriodService.java b/financer-server/src/main/java/de/financer/service/PeriodService.java index aa3c103..5c4a93c 100644 --- a/financer-server/src/main/java/de/financer/service/PeriodService.java +++ b/financer-server/src/main/java/de/financer/service/PeriodService.java @@ -2,6 +2,7 @@ package de.financer.service; import de.financer.ResponseReason; import de.financer.dba.PeriodRepository; +import de.financer.dto.PeriodOverviewDto; import de.financer.model.Period; import de.financer.model.PeriodType; import de.financer.model.Transaction; @@ -14,6 +15,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.List; import java.util.Optional; @Service @@ -139,4 +141,8 @@ public class PeriodService { public Optional getPeriodById(Long id) { return this.periodRepository.findById(id); } + + public List getPeriodOverview() { + return this.periodRepository.getPeriodOverview(); + } } diff --git a/financer-server/src/main/resources/native_queries/folder-info.txt b/financer-server/src/main/resources/native_queries/folder-info.txt new file mode 100644 index 0000000..9bf72ed --- /dev/null +++ b/financer-server/src/main/resources/native_queries/folder-info.txt @@ -0,0 +1,10 @@ +This folder contains DBMS-agnostic native queries. + +Reasons for doing it like this: +- Queries in here are quite big and expressing those either with Criteria API or HQL/JPQL is a PITA +- Easy formatting for readability +- Actually SQL files + +Downsides: +- No compile-time check via e.g. the static meta model created during build, but will be mitigated with Unit tests +- Quite unique infrastructure to integrate these queries into the Spring repositories (SqlResultSetMapping, NativeQueries) \ No newline at end of file diff --git a/financer-server/src/main/resources/native_queries/period_overview.sql b/financer-server/src/main/resources/native_queries/period_overview.sql new file mode 100644 index 0000000..0912d4a --- /dev/null +++ b/financer-server/src/main/resources/native_queries/period_overview.sql @@ -0,0 +1,130 @@ +WITH income AS ( + -- Regular income based on INCOME accounts + SELECT p.ID, SUM(asIncome.spending_total_from) AS incomeSum + FROM period p + INNER JOIN account_statistic asIncome ON asIncome.period_id = p.id + INNER JOIN account aIncome ON aIncome.id = asIncome.account_id AND aIncome.type = 'INCOME' + GROUP BY p.id, p.type, p.start, p."end" +), +incomeCredit as ( + -- Special case for credits that can be booked from a LIABILITY account to a BANK/CASH account + -- Need to be counted as income as the money is used for expenses and those expenses will be counted + -- as expense, so to make it even we need to count it here as well + SELECT p2.id, SUM(amount) AS incomeCreditSum + FROM "transaction" t + INNER JOIN account a on a.id = t.from_account_id + INNER JOIN account a2 on a2.id = t.to_account_id + INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id + INNER JOIN period p2 on p2.id = ltp.period_id + WHERE 1 = 1 + AND a.type in ('LIABILITY') + AND a2.type in ('BANK', 'CASH') + GROUP BY p2.id, p2.type, p2.start, p2."end" +), +incomeStart as ( + -- Special case for money that was there at the starting time of a financer instance + -- Will be counted as income as this money is used for expanses, so to make it even + -- we need to count it here as well + SELECT p2.id, SUM(amount) AS incomeStartSum + FROM "transaction" t + INNER JOIN account a on a.id = t.from_account_id + INNER JOIN account a2 on a2.id = t.to_account_id + INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id + INNER JOIN period p2 on p2.id = ltp.period_id + WHERE 1 = 1 + AND a.type in ('START') + AND a2.type in ('BANK', 'CASH') + GROUP BY p2.id, p2.type, p2.start, p2."end" +), +expense AS ( + -- Expense booking - NOT counted is the case LIABILITY -> EXPENSE even though that is a + -- valid booking in the app. This is because we would count the expense once here and a second time + -- with the liability query + SELECT p2.id, SUM(amount) AS expenseSum + FROM "transaction" t + INNER JOIN account a on a.id = t.from_account_id + INNER JOIN account a2 on a2.id = t.to_account_id + INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id + INNER JOIN period p2 on p2.id = ltp.period_id + WHERE 1 = 1 + AND a.type in ('BANK', 'CASH') + AND a2.type in ('EXPENSE') + GROUP BY p2.id, p2.type, p2.start, p2."end" +), +liability AS ( + -- Excluded is the special case for start bookings, START -> LIABILITY + -- as the actual expense for that was some time in the past before the starting + -- of the financer instance + SELECT p2.id, SUM(amount) AS liabilitySum + FROM "transaction" t + INNER JOIN account a on a.id = t.from_account_id + INNER JOIN account a2 on a2.id = t.to_account_id + INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id + INNER JOIN period p2 on p2.id = ltp.period_id + WHERE 1 = 1 + AND a.type in ('BANK', 'CASH') + AND a2.type in ('LIABILITY') + GROUP BY p2.id, p2.type, p2.start, p2."end" +), +assets AS ( + -- Returns only the assets for closed periods + SELECT p.ID, SUM(asBankCash.end_balance) AS assetSum + FROM period p + INNER JOIN account_statistic asBankCash ON asBankCash.period_id = p.id + INNER JOIN account aBankCash ON aBankCash.id = asBankCash.account_id AND aBankCash.type IN ('BANK', 'CASH') + GROUP BY p.id, p.type, p.start, p."end" +), +transactions AS ( + -- The count of transactions in a period + SELECT ltp.period_id, COUNT(*) AS transaction_count + FROM link_transaction_period ltp + GROUP BY ltp.period_id +) +SELECT + p.ID PERIOD_ID, + p.type PERIOD_TYPE, + p.start PERIOD_START, + p."end" PERIOD_END, + CASE + -- 2^3 possible cases + WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN 0 + WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN i.incomeSum + WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NULL THEN ic.incomeCreditSum + WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NULL THEN (i.incomeSum + ic.incomeCreditSum) + WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NOT NULL THEN "is".incomeStartSum + WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NOT NULL THEN (i.incomeSum + "is".incomeStartSum) + WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NOT NULL THEN (ic.incomeCreditSum + "is".incomeStartSum) + WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NOT NULL THEN (i.incomeSum + ic.incomeCreditSum + "is".incomeStartSum) + END INCOME_SUM, + CASE + WHEN e.expenseSum IS NULL THEN 0 + WHEN e.expenseSum IS NOT NULL THEN e.expenseSum + END EXPENSE_SUM, + CASE + WHEN l.liabilitySum IS NULL THEN 0 + WHEN l.liabilitySum IS NOT NULL THEN l.liabilitySum + END LIABILITY_SUM, + CASE + -- 2^2 possible cases + WHEN e.expenseSum IS NULL AND l.liabilitySum IS NULL THEN 0 + WHEN e.expenseSum IS NOT NULL AND l.liabilitySum IS NULL THEN e.expenseSum + WHEN e.expenseSum IS NULL AND l.liabilitySum IS NOT NULL THEN l.liabilitySum + WHEN e.expenseSum IS NOT NULL AND l.liabilitySum IS NOT NULL THEN (e.expenseSum + l.liabilitySum) + END TOTAL, + a.assetSum ASSETS_SUM, + CASE + WHEN t.transaction_count IS NULL THEN 0 + WHEN t.transaction_count IS NOT NULL THEN t.transaction_count + END TRANSACTION_COUNT +FROM + period p +LEFT JOIN income i ON i.ID = p.ID +LEFT JOIN incomeCredit ic ON ic.ID = p.ID +LEFT JOIN incomeStart "is" ON "is".ID = p.ID +LEFT JOIN expense e ON e.ID = p.ID +LEFT JOIN assets a ON a.ID = p.ID +LEFT JOIN liability l ON l.ID = p.ID +LEFT JOIN transactions t ON t.period_id = p.ID +ORDER BY + "end" DESC, + start ASC; \ No newline at end of file diff --git a/financer-server/src/test/java/de/financer/controller/integration/PeriodController_getPeriodOverviewIntegrationTest.java b/financer-server/src/test/java/de/financer/controller/integration/PeriodController_getPeriodOverviewIntegrationTest.java new file mode 100644 index 0000000..6245da8 --- /dev/null +++ b/financer-server/src/test/java/de/financer/controller/integration/PeriodController_getPeriodOverviewIntegrationTest.java @@ -0,0 +1,50 @@ +package de.financer.controller.integration; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.financer.FinancerApplication; +import de.financer.dto.PeriodOverviewDto; +import de.financer.model.Account; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = FinancerApplication.class) +@AutoConfigureMockMvc +@TestPropertySource( + locations = "classpath:application-integrationtest.properties") +public class PeriodController_getPeriodOverviewIntegrationTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + public void test_getPeriodOverview() throws Exception { + final MvcResult mvcResult = this.mockMvc + .perform(get("/periods/getPeriodOverview").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + final List periodOverview = this.objectMapper + .readValue(mvcResult.getResponse().getContentAsByteArray(), new TypeReference>() {}); + + // No results in DB, we just want to test the execution of the query because it is native + Assert.assertEquals(0, periodOverview.size()); + } +} diff --git a/financer-web-client/src/main/java/de/financer/controller/Function.java b/financer-web-client/src/main/java/de/financer/controller/Function.java index 34c55bc..fb47553 100644 --- a/financer-web-client/src/main/java/de/financer/controller/Function.java +++ b/financer-web-client/src/main/java/de/financer/controller/Function.java @@ -34,6 +34,7 @@ public enum Function { P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"), P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod"), + P_GET_PERIOD_OVERVIEW("periods/getPeriodOverview"), FILE_GET("file"); diff --git a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java index dfa9938..7c4b70e 100644 --- a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java +++ b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java @@ -1,13 +1,21 @@ package de.financer.controller; import de.financer.config.FinancerConfig; +import de.financer.dto.PeriodOverviewDto; +import de.financer.dto.SearchTransactionsResponseDto; +import de.financer.template.FinancerRestTemplate; import de.financer.template.StringTemplate; +import de.financer.template.exception.FinancerRestException; import de.financer.util.ControllerUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.util.UriComponentsBuilder; +import java.util.List; + @Controller public class PeriodController { @Autowired @@ -22,4 +30,27 @@ public class PeriodController { return "redirect:/accountOverview"; } + + @GetMapping("/periodOverview") + public String periodOverview(Model model) { + final UriComponentsBuilder periodBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.P_GET_PERIOD_OVERVIEW)); + + try { + final List periodOverviews = FinancerRestTemplate.exchangeGet(periodBuilder, + new ParameterizedTypeReference>() { + }); + + model.addAttribute("periodOverviews", periodOverviews); + } catch (FinancerRestException e) { + // TODO + model.addAttribute("errorMessage", e.getResponseReason().name()); + } + + ControllerUtils.addVersionAttribute(model, this.financerConfig); + ControllerUtils.addCurrencySymbol(model, this.financerConfig); + ControllerUtils.addDarkMode(model, this.financerConfig); + + return "period/periodOverview"; + } } diff --git a/financer-web-client/src/main/resources/i18n/message.properties b/financer-web-client/src/main/resources/i18n/message.properties index 5167c9f..85a4cba 100644 --- a/financer-web-client/src/main/resources/i18n/message.properties +++ b/financer-web-client/src/main/resources/i18n/message.properties @@ -11,6 +11,7 @@ financer.account-overview.available-actions.create-account-group=Create new acco financer.account-overview.available-actions.select-chart=Generate a chart financer.account-overview.available-actions.close-current-period=Close the current expense period financer.account-overview.available-actions.recurring-transaction-calendar=Recurring transaction calendar +financer.account-overview.available-actions.period-overview=Period overview financer.account-overview.status=Status\: financer.account-overview.status.recurring-transaction-due-today=Recurring transactions due today\: financer.account-overview.status.recurring-transaction-active=Active recurring transactions\: @@ -172,6 +173,18 @@ financer.chart-config-account-expenses-for-period.label.from-date=From date\: financer.chart-config-account-expenses-for-period.label.to-date=To date\: financer.chart-config-account-expenses-for-period.submit=Generate +financer.period-overview.title=Period overview +financer.period-overview.table-header.id=ID +financer.period-overview.table-header.type=Period type +financer.period-overview.table-header.start=Start +financer.period-overview.table-header.end=End +financer.period-overview.table-header.income=Income +financer.period-overview.table-header.expense=Expenses +financer.period-overview.table-header.liability=Liabilities +financer.period-overview.table-header.total=Total +financer.period-overview.table-header.assets=Assets +financer.period-overview.table-header.transactions=Transaction count + financer.interval-type.DAILY=Daily financer.interval-type.WEEKLY=Weekly financer.interval-type.MONTHLY=Monthly @@ -193,6 +206,10 @@ financer.account-type.START=Start financer.account-status.OPEN=Open financer.account-status.CLOSED=Closed +financer.period-type.EXPENSE=Expense +financer.period-type.EXPENSE_YEAR=Expense year +financer.period-type.GRAND_TOTAL=Grand total + financer.heading.transaction-new=financer\: create new transaction financer.heading.recurring-transaction-new=financer\: create new recurring transaction financer.heading.account-new=financer\: create new account @@ -208,6 +225,7 @@ financer.heading.chart-config-account-group-expenses-for-period=financer\: confi financer.heading.chart-config-account-expenses-for-period=financer\: configure account expenses for period chart financer.heading.search-transactions=financer\: search transactions financer.heading.recurring-transaction-calendar=financer\: recurring transaction calendar +financer.heading.period-overview=financer\: period overview financer.cancel-back-to-overview=Cancel and back to overview financer.back-to-overview=Back to overview diff --git a/financer-web-client/src/main/resources/i18n/message_de_DE.properties b/financer-web-client/src/main/resources/i18n/message_de_DE.properties index bd87d35..33cd2e8 100644 --- a/financer-web-client/src/main/resources/i18n/message_de_DE.properties +++ b/financer-web-client/src/main/resources/i18n/message_de_DE.properties @@ -11,6 +11,7 @@ financer.account-overview.available-actions.create-account-group=Neue Konto-Grup financer.account-overview.available-actions.select-chart=Ein Diagramm erzeugen financer.account-overview.available-actions.close-current-period=Aktuelle Periode schlie\u00DFen financer.account-overview.available-actions.recurring-transaction-calendar=Kalender wiederkehrende Buchung +financer.account-overview.available-actions.period-overview=Perioden\u00FCbersicht financer.account-overview.status=Status\: financer.account-overview.status.recurring-transaction-due-today=Wiederkehrende Buchungen heute f\u00E4llig\: financer.account-overview.status.recurring-transaction-active=Aktive wiederkehrende Buchungen\: @@ -172,6 +173,18 @@ financer.chart-config-account-expenses-for-period.label.from-date=Von Datum\: financer.chart-config-account-expenses-for-period.label.to-date=Bis Datum\: financer.chart-config-account-expenses-for-period.submit=Erzeugen +financer.period-overview.title=Perioden\u00FCbersicht +financer.period-overview.table-header.id=ID +financer.period-overview.table-header.type=Periodentyp +financer.period-overview.table-header.start=Start +financer.period-overview.table-header.end=Ende +financer.period-overview.table-header.income=Einkommen +financer.period-overview.table-header.expense=Ausgaben +financer.period-overview.table-header.liability=Verbindlichkeiten +financer.period-overview.table-header.total=Insgesamt +financer.period-overview.table-header.assets=Umlaufverm\u00F6gen +financer.period-overview.table-header.transactions=Anzahl Buchungen + financer.interval-type.DAILY=T\u00E4glich financer.interval-type.WEEKLY=W\u00F6chentlich financer.interval-type.MONTHLY=Monatlich @@ -193,6 +206,10 @@ financer.account-type.START=Anfangsbestand financer.account-status.OPEN=Offen financer.account-status.CLOSED=Geschlossen +financer.period-type.EXPENSE=Ausgaben +financer.period-type.EXPENSE_YEAR=Jahresausgaben +financer.period-type.GRAND_TOTAL=Gesamtausgaben + financer.heading.transaction-new=financer\: Neue Buchung erstellen financer.heading.recurring-transaction-new=financer\: Neue wiederkehrende Buchung erstellen financer.heading.account-new=financer\: Neues Konto erstellen @@ -207,6 +224,7 @@ financer.heading.chart-select=financer\: Ein Diagramm zum Erzeugen ausw\u00E4hle financer.heading.chart-config-account-group-expenses-for-period=financer\: Konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm financer.heading.search-transactions=financer\: Buchungen suchen financer.heading.recurring-transaction-calendar=financer\: Kalender wiederkehrende Buchung +financer.heading.period-overview=financer\: Perioden\u00FCbersicht financer.cancel-back-to-overview=Abbrechen und zur\u00FCck zur \u00DCbersicht financer.back-to-overview=Zur\u00FCck zur \u00DCbersicht diff --git a/financer-web-client/src/main/resources/static/css/main.css b/financer-web-client/src/main/resources/static/css/main.css index 888079e..dbe4f2a 100644 --- a/financer-web-client/src/main/resources/static/css/main.css +++ b/financer-web-client/src/main/resources/static/css/main.css @@ -34,7 +34,8 @@ a { #account-overview-table, #transaction-table, -#recurring-transaction-list-table { +#recurring-transaction-list-table, +#period-overview-table { width: 100%; border-collapse: collapse; text-align: left; @@ -47,7 +48,9 @@ a { #transaction-table th, #transaction-table td, #recurring-transaction-list-table th, -#recurring-transaction-list-table td { +#recurring-transaction-list-table td, +#period-overview-table th, +#period-overview-table td { border-bottom: 1px solid var(--border-color); padding: 0.3em; vertical-align: top; @@ -55,7 +58,8 @@ a { #account-overview-table th, #transaction-table th, -#recurring-transaction-list-table th { +#recurring-transaction-list-table th, +#period-overview-table th { position: sticky; top: 0px; background-color: var(--background-color); diff --git a/financer-web-client/src/main/resources/templates/account/accountOverview.html b/financer-web-client/src/main/resources/templates/account/accountOverview.html index 06ae5f8..e39f50c 100644 --- a/financer-web-client/src/main/resources/templates/account/accountOverview.html +++ b/financer-web-client/src/main/resources/templates/account/accountOverview.html @@ -66,6 +66,8 @@
+ + + + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/> + <link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/> + <link rel="stylesheet" th:href="@{/css/main.css}"> + <link rel="shortcut icon" th:href="@{/favicon.ico}"/> +</head> +<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}"/> +<table id="period-overview-table"> + <tr> + <th th:text="#{financer.period-overview.table-header.id}"/> + <th th:text="#{financer.period-overview.table-header.type}"/> + <th th:text="#{financer.period-overview.table-header.start}"/> + <th th:text="#{financer.period-overview.table-header.end}"/> + <th th:text="#{financer.period-overview.table-header.income}" /> + <th th:text="#{financer.period-overview.table-header.expense}"/> + <th th:text="#{financer.period-overview.table-header.liability}"/> + <th th:text="#{financer.period-overview.table-header.total}"/> + <th th:text="#{financer.period-overview.table-header.assets}"/> + <th th:text="#{financer.period-overview.table-header.transactions}"/> + </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:utext="${#numbers.formatDecimal(periodOverview.total/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}" + th:classappend="${periodOverview.total > periodOverview.incomeSum} ? overspend"/> + <td th:if="${periodOverview.assetsSum != null}" + th:utext="${#numbers.formatDecimal(periodOverview.assetsSum/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/> + <td th:if="${periodOverview.assetsSum == null}" /> + <td th:text="${periodOverview.transactionCount}"/> + </tr> +</table> +<div th:replace="includes/footer :: footer"/> +</body> +</html> \ No newline at end of file