WIP Initial commit for financer web client

This commit is contained in:
2019-03-24 23:28:15 +01:00
commit 67bd7f951a
43 changed files with 1999 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
financer-web-client.log*
.attach*

141
pom.xml Normal file
View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>de.77zzcx7.financer</groupId>
<artifactId>financer-web-client</artifactId>
<version>1-SNAPSHOT</version>
<packaging>${packaging.type}</packaging>
<description>The web client part of the financer application - a simple app to manage your personal finances</description>
<name>financer-web-client</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.9</maven.compiler.source>
<maven.compiler.target>1.9</maven.compiler.target>
<java.version>1.9</java.version>
<packaging.type>jar</packaging.type>
<activeProfiles>dev</activeProfiles>
<!-- Property to define the parallel deployment version.
See e.g. https://tomcat.apache.org/tomcat-8.5-doc/config/context.html
Should be the same as the project.version but padded with zeros to six digits. -->
<parallelDeploymentVersion>000001</parallelDeploymentVersion>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Apache commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.3</version>
</dependency>
<!-- Misc dependencies -->
<!-- Runtime dependencies -->
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*IntegrationTest</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>integration-tests</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<executions>
<execution>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<excludes>
<exclude>none</exclude>
</excludes>
<includes>
<include>**/*IntegrationTest</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>build-war</id>
<properties>
<packaging.type>war</packaging.type>
<activeProfiles>postgres</activeProfiles>
</properties>
<build>
<finalName>${project.artifactId}##${parallelDeploymentVersion}</finalName>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,20 @@
package de.financer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@SpringBootApplication
public class FinancerApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(FinancerApplication.class);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(FinancerApplication.class);
}
}

View File

@@ -0,0 +1,53 @@
package de.financer;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public enum ResponseReason {
OK(HttpStatus.OK),
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_DATE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR),
AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_AMOUNT(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_BOOKING_ACCOUNTS(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_FIRST_OCCURRENCE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
private HttpStatus httpStatus;
ResponseReason(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public ResponseEntity toResponseEntity() {
return new ResponseEntity<>(this.name(), this.httpStatus);
}
public static ResponseReason fromResponseEntity(ResponseEntity<String> entity) {
for (ResponseReason reason : values()) {
if (reason.name().equals(entity.getBody())) {
return reason;
}
}
return UNKNOWN_ERROR;
}
}

View File

@@ -0,0 +1,31 @@
package de.financer.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "financer")
public class FinancerConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(FinancerConfig.class);
private String serverUrl;
private String dateFormat;
public String getServerUrl() {
return serverUrl;
}
public void setServerUrl(String serverUrl) {
this.serverUrl = serverUrl;
}
public String getDateFormat() {
return dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
}

View File

@@ -0,0 +1,137 @@
package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.*;
import de.financer.form.NewAccountForm;
import de.financer.model.*;
import de.financer.util.ControllerUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
@Controller
public class AccountController {
@Autowired
private FinancerConfig financerConfig;
@GetMapping("/accountOverview")
public String accountOverview(String showClosed, Model model) {
final ResponseEntity<Iterable<Account>> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
final ResponseEntity<Iterable<RecurringTransaction>> rtDtRes = new GetAllRecurringTransactionsDueTodayTemplate()
.exchange(this.financerConfig);
final ResponseEntity<Iterable<RecurringTransaction>> rtAllActRes = new GetAllActiveRecurringTransactionsTemplate()
.exchange(this.financerConfig);
final boolean showClosedBoolean = BooleanUtils.toBoolean(showClosed);
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody(), showClosedBoolean));
model.addAttribute("rtDueTodayCount", IterableUtils.size(rtDtRes.getBody()));
model.addAttribute("rtAllActiveCount", IterableUtils.size(rtAllActRes.getBody()));
model.addAttribute("showClosed", showClosedBoolean);
return "account/accountOverview";
}
@GetMapping("/newAccount")
public String newAccount(Model model) {
model.addAttribute("accounttypes", AccountType.valueList());
model.addAttribute("newAccountForm", new NewAccountForm());
return "account/newAccount";
}
@PostMapping("/saveAccount")
public String saveAccont(NewAccountForm newAccountForm, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT))
.queryParam("key", newAccountForm.getKey())
.queryParam("type", newAccountForm.getType());
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
if (!ResponseReason.OK.equals(responseReason)) {
model.addAttribute("form", newAccountForm);
model.addAttribute("accounttypes", AccountType.valueList());
model.addAttribute("errorMessage", responseReason.name());
return "account/newAccount";
}
return "redirect:/accountOverview";
}
@GetMapping("/accountDetails")
public String accountDetails(String key, Model model) {
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
final Account account = response.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
model.addAttribute("account", account);
model.addAttribute("transactions", IterableUtils.toList(transactionResponse.getBody()));
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
return "account/accountDetails";
}
@GetMapping("/closeAccount")
public String closeAccount(String key, Model model) {
final UriComponentsBuilder closeBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CLOSE_ACCOUNT))
.queryParam("key", key);
final ResponseEntity<String> closeResponse = new StringTemplate().exchange(closeBuilder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
final Account account = response.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
model.addAttribute("account", account);
model.addAttribute("transactions", IterableUtils.toList(transactionResponse.getBody()));
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
model.addAttribute("errorMessage", responseReason.name());
return "account/accountDetails";
}
return "redirect:/accountOverview";
}
@GetMapping("/openAccount")
public String openAccount(String key, Model model) {
final UriComponentsBuilder openBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_OPEN_ACCOUNT))
.queryParam("key", key);
final ResponseEntity<String> closeResponse = new StringTemplate().exchange(openBuilder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
final Account account = response.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
model.addAttribute("account", account);
model.addAttribute("transactions", IterableUtils.toList(transactionResponse.getBody()));
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
model.addAttribute("errorMessage", responseReason.name());
return "account/accountDetails";
}
return "redirect:/accountOverview";
}
}

View File

@@ -0,0 +1,32 @@
package de.financer.controller;
public enum Function {
ACC_GET_BY_KEY("accounts/getByKey"),
ACC_GET_ALL("accounts/getAll"),
ACC_CREATE_ACCOUNT("accounts/createAccount"),
ACC_CLOSE_ACCOUNT("accounts/closeAccount"),
ACC_OPEN_ACCOUNT("accounts/openAccount"),
TR_GET_ALL("transactions/getAll"),
TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"),
TR_CREATE_TRANSACTION("transactions/createTransaction"),
TR_DELETE_TRANSACTION("transactions/deleteTransaction"),
RT_GET_ALL("recurringTransactions/getAll"),
RT_GET_ALL_ACTIVE("recurringTransactions/getAllActive"),
RT_GET_ALL_FOR_ACCOUNT("recurringTransactions/getAllForAccount"),
RT_GET_ALL_DUE_TODAY("recurringTransactions/getAllDueToday"),
RT_CREATE_RECURRING_TRANSACTION("recurringTransactions/createRecurringTransaction"),
RT_DELETE_RECURRING_TRANSACTION("recurringTransactions/deleteRecurringTransaction"),
RT_CREATE_TRANSACTION("recurringTransactions/createTransaction");
private String path;
Function(String path) {
this.path = path;
}
public String getPath() {
return this.path;
}
}

View File

@@ -0,0 +1,113 @@
package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.*;
import de.financer.form.NewRecurringTransactionForm;
import de.financer.model.Account;
import de.financer.model.HolidayWeekendType;
import de.financer.model.IntervalType;
import de.financer.model.RecurringTransaction;
import de.financer.util.ControllerUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
@Controller
public class RecurringTransactionController {
@Autowired
private FinancerConfig financerConfig;
@GetMapping("/newRecurringTransaction")
public String newRecurringTransaction(Model model) {
final ResponseEntity<Iterable<Account>> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody()));
model.addAttribute("intervalTypes", IntervalType.valueList());
model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList());
model.addAttribute("newRecurringTransactionForm", new NewRecurringTransactionForm());
return "recurringTransaction/newRecurringTransaction";
}
@PostMapping("/saveRecurringTransaction")
public String saveRecurringTransaction(NewRecurringTransactionForm newRecurringTransactionForm, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.RT_CREATE_RECURRING_TRANSACTION))
.queryParam("fromAccountKey", newRecurringTransactionForm
.getFromAccountKey())
.queryParam("toAccountKey", newRecurringTransactionForm
.getToAccountKey())
.queryParam("amount", newRecurringTransactionForm
.getAmount())
.queryParam("firstOccurrence", ControllerUtils
.formatDate(this.financerConfig, newRecurringTransactionForm
.getFirstOccurrence()))
.queryParam("lastOccurrence", ControllerUtils
.formatDate(this.financerConfig, newRecurringTransactionForm
.getLastOccurrence()))
.queryParam("holidayWeekendType", newRecurringTransactionForm
.getHolidayWeekendType())
.queryParam("intervalType", newRecurringTransactionForm
.getIntervalType())
.queryParam("description", newRecurringTransactionForm
.getDescription());
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Iterable<Account>> getAllResponse = new GetAllAccountsTemplate()
.exchange(this.financerConfig);
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(getAllResponse.getBody()));
model.addAttribute("intervalTypes", IntervalType.valueList());
model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList());
model.addAttribute("form", newRecurringTransactionForm);
model.addAttribute("errorMessage", responseReason.name());
return "recurringTransaction/newRecurringTransaction";
}
return "redirect:/accountOverview";
}
@GetMapping("/recurringTransactionDueToday")
public String recurringTransactionDueToday(Model model) {
final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllRecurringTransactionsDueTodayTemplate()
.exchange(this.financerConfig);
model.addAttribute("recurringTransactions", response.getBody());
model.addAttribute("subTitle", "dueToday");
return "recurringTransaction/recurringTransactionList";
}
@GetMapping("/recurringTransactionActive")
public String recurringTransactionActive(Model model) {
final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllActiveRecurringTransactionsTemplate()
.exchange(this.financerConfig);
model.addAttribute("recurringTransactions", response.getBody());
model.addAttribute("subTitle", "active");
return "recurringTransaction/recurringTransactionList";
}
@GetMapping("/recurringTransactionAll")
public String recurringTransactionAll(Model model) {
final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllRecurringTransactionsTemplate()
.exchange(this.financerConfig);
model.addAttribute("recurringTransactions", response.getBody());
model.addAttribute("subTitle", "all");
return "recurringTransaction/recurringTransactionList";
}
}

View File

@@ -0,0 +1,59 @@
package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.GetAllAccountsTemplate;
import de.financer.controller.template.StringTemplate;
import de.financer.form.NewTransactionForm;
import de.financer.model.Account;
import de.financer.util.ControllerUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
@Controller
public class TransactionController {
@Autowired
private FinancerConfig financerConfig;
@GetMapping("/newTransaction")
public String newTransaction(Model model) {
final ResponseEntity<Iterable<Account>> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody()));
model.addAttribute("newTransactionForm", new NewTransactionForm());
return "transaction/newTransaction";
}
@PostMapping("/saveTransaction")
public String saveTransaction(NewTransactionForm newTransactionForm, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_CREATE_TRANSACTION))
.queryParam("fromAccountKey", newTransactionForm.getFromAccountKey())
.queryParam("toAccountKey", newTransactionForm.getToAccountKey())
.queryParam("amount", newTransactionForm.getAmount())
.queryParam("date", ControllerUtils.formatDate(this.financerConfig, newTransactionForm.getDate()))
.queryParam("description", newTransactionForm.getDescription());
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Iterable<Account>> accRes = new GetAllAccountsTemplate().exchange(this.financerConfig);
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(accRes.getBody()));
model.addAttribute("form", newTransactionForm);
model.addAttribute("errorMessage", responseReason.name());
return "transaction/newTransaction";
}
return "redirect:/accountOverview";
}
}

View File

@@ -0,0 +1,18 @@
package de.financer.controller.handler;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import java.io.IOException;
public class NoExceptionResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}

View File

@@ -0,0 +1,42 @@
package de.financer.controller.template;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.financer.controller.handler.NoExceptionResponseErrorHandler;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
public class FinancerRestTemplate<T> {
public ResponseEntity<T> exchange(String url, ParameterizedTypeReference<T> type) {
final RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
return restTemplate.exchange(url, HttpMethod.GET, null, type);
}
public ResponseEntity<T> exchange(String url, Class<T> type) {
final RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
return restTemplate.exchange(url, HttpMethod.GET, null, type);
}
// The spring.jackson. properties are not picked up by the RestTemplate and its converter,
// so we need to overwrite it here
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
}

View File

@@ -0,0 +1,21 @@
package de.financer.controller.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.Account;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class GetAccountByKeyTemplate {
public ResponseEntity<Account> exchange(FinancerConfig financerConfig, String key) {
final UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(ControllerUtils
.buildUrl(financerConfig, Function.ACC_GET_BY_KEY))
.queryParam("key", key);
return new FinancerRestTemplate<Account>()
.exchange(builder.toUriString(), new ParameterizedTypeReference<Account>() {
});
}
}

View File

@@ -0,0 +1,16 @@
package de.financer.controller.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.Account;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
public class GetAllAccountsTemplate {
public ResponseEntity<Iterable<Account>> exchange(FinancerConfig financerConfig) {
return new FinancerRestTemplate<Iterable<Account>>().exchange(ControllerUtils
.buildUrl(financerConfig, Function.ACC_GET_ALL), new ParameterizedTypeReference<Iterable<Account>>() {
});
}
}

View File

@@ -0,0 +1,16 @@
package de.financer.controller.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.RecurringTransaction;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
public class GetAllActiveRecurringTransactionsTemplate {
public ResponseEntity<Iterable<RecurringTransaction>> exchange(FinancerConfig financerConfig) {
return new FinancerRestTemplate<Iterable<RecurringTransaction>>().exchange(ControllerUtils
.buildUrl(financerConfig, Function.RT_GET_ALL_ACTIVE), new ParameterizedTypeReference<Iterable<RecurringTransaction>>() {
});
}
}

View File

@@ -0,0 +1,16 @@
package de.financer.controller.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.RecurringTransaction;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
public class GetAllRecurringTransactionsDueTodayTemplate {
public ResponseEntity<Iterable<RecurringTransaction>> exchange(FinancerConfig financerConfig) {
return new FinancerRestTemplate<Iterable<RecurringTransaction>>().exchange(ControllerUtils
.buildUrl(financerConfig, Function.RT_GET_ALL_DUE_TODAY), new ParameterizedTypeReference<Iterable<RecurringTransaction>>() {
});
}
}

View File

@@ -0,0 +1,16 @@
package de.financer.controller.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.RecurringTransaction;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
public class GetAllRecurringTransactionsTemplate {
public ResponseEntity<Iterable<RecurringTransaction>> exchange(FinancerConfig financerConfig) {
return new FinancerRestTemplate<Iterable<RecurringTransaction>>().exchange(ControllerUtils
.buildUrl(financerConfig, Function.RT_GET_ALL), new ParameterizedTypeReference<Iterable<RecurringTransaction>>() {
});
}
}

View File

@@ -0,0 +1,21 @@
package de.financer.controller.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.Transaction;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class GetAllTransactionsForAccountTemplate {
public ResponseEntity<Iterable<Transaction>> exchange(FinancerConfig financerConfig, String accountKey) {
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_GET_ALL_FOR_ACCOUNT))
.queryParam("accountKey", accountKey);
return new FinancerRestTemplate<Iterable<Transaction>>()
.exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference<Iterable<Transaction>>() {
});
}
}

View File

@@ -0,0 +1,13 @@
package de.financer.controller.template;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class StringTemplate {
public ResponseEntity<String> exchange(UriComponentsBuilder builder) {
return new FinancerRestTemplate<String>()
.exchange(builder.toUriString(), new ParameterizedTypeReference<String>() {
});
}
}

View File

@@ -0,0 +1,22 @@
package de.financer.form;
public class NewAccountForm {
private String key;
private String type;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View File

@@ -0,0 +1,76 @@
package de.financer.form;
public class NewRecurringTransactionForm {
private String fromAccountKey;
private String toAccountKey;
private String amount;
private String description;
private String firstOccurrence;
private String lastOccurrence;
private String intervalType;
private String holidayWeekendType;
public String getFromAccountKey() {
return fromAccountKey;
}
public void setFromAccountKey(String fromAccountKey) {
this.fromAccountKey = fromAccountKey;
}
public String getToAccountKey() {
return toAccountKey;
}
public void setToAccountKey(String toAccountKey) {
this.toAccountKey = toAccountKey;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getFirstOccurrence() {
return firstOccurrence;
}
public void setFirstOccurrence(String firstOccurrence) {
this.firstOccurrence = firstOccurrence;
}
public String getLastOccurrence() {
return lastOccurrence;
}
public void setLastOccurrence(String lastOccurrence) {
this.lastOccurrence = lastOccurrence;
}
public String getIntervalType() {
return intervalType;
}
public void setIntervalType(String intervalType) {
this.intervalType = intervalType;
}
public String getHolidayWeekendType() {
return holidayWeekendType;
}
public void setHolidayWeekendType(String holidayWeekendType) {
this.holidayWeekendType = holidayWeekendType;
}
}

View File

@@ -0,0 +1,49 @@
package de.financer.form;
public class NewTransactionForm {
private String fromAccountKey;
private String toAccountKey;
private String amount;
private String date;
private String description;
public String getFromAccountKey() {
return fromAccountKey;
}
public void setFromAccountKey(String fromAccountKey) {
this.fromAccountKey = fromAccountKey;
}
public String getToAccountKey() {
return toAccountKey;
}
public void setToAccountKey(String toAccountKey) {
this.toAccountKey = toAccountKey;
}
public String getAmount() {
return amount;
}
public void setAmount(String amount) {
this.amount = amount;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -0,0 +1,45 @@
package de.financer.model;
public class Account {
private Long id;
private String key;
private AccountType type;
private AccountStatus status;
private Long currentBalance;
public Long getId() {
return id;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public AccountType getType() {
return type;
}
public void setType(AccountType type) {
this.type = type;
}
public AccountStatus getStatus() {
return status;
}
public void setStatus(AccountStatus status) {
this.status = status;
}
public Long getCurrentBalance() {
return currentBalance;
}
public void setCurrentBalance(Long currentBalance) {
this.currentBalance = currentBalance;
}
}

View File

@@ -0,0 +1,20 @@
package de.financer.model;
import java.util.Arrays;
public enum AccountStatus {
/** Indicates that the account is open for bookings */
OPEN,
/** Indicates that the account is closed and bookings to it are forbidden */
CLOSED;
/**
* This method validates whether the given string represents a valid account status.
*
* @param status to check
* @return whether the given status represents a valid account status
*/
public static boolean isValidType(String status) {
return Arrays.stream(AccountStatus.values()).anyMatch((accountStatus) -> accountStatus.name().equals(status));
}
}

View File

@@ -0,0 +1,38 @@
package de.financer.model;
import java.util.*;
import java.util.stream.Collectors;
public enum AccountType {
/** Used to mark an account that acts as a source of money, e.g. monthly wage */
INCOME,
/** Indicates a real account at a bank, e.g. a check payment account */
BANK,
/** Marks an account as physical cash, e.g. the money currently in the purse */
CASH,
/** Used to mark an account that acts as a destination of money, e.g. through buying goods */
EXPENSE,
/** Marks an account as a liability from a third party, e.g. credit card or loan */
LIABILITY,
/** Marks the start account that is to be used to book all the opening balances for the different accounts */
START;
/**
* This method validates whether the given string represents a valid account type.
*
* @param type to check
* @return whether the given type represents a valid account type
*/
public static boolean isValidType(String type) {
return Arrays.stream(AccountType.values()).anyMatch((accountType) -> accountType.name().equals(type));
}
public static List<String> valueList() {
return Arrays.stream(AccountType.values()).map(AccountType::name).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,71 @@
package de.financer.model;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* This enum specifies constants that control how actions should be handled that would fall on a holiday
* or weekday (where usually are no bookings done by e.g. banks)
*/
public enum HolidayWeekendType {
/** Indicates that the action should be done on the specified day regardless whether it's a holiday or a weekend */
SAME_DAY,
/**
* <p>
* Indicates that the action should be deferred to the next workday.
* </p>
* <pre>
* Example 1:
* MO TU WE TH FR SA SO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Deferred, effective due date of action
* </pre>
* <pre>
* Example 2:
* TU WE TH FR SA SO MO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Deferred, effective due date of action
* </pre>
*
*/
NEXT_WORKDAY,
/**
* <p>
* Indicates that the action should preponed to the previous day
* </p>
* <pre>
* Example 1:
* MO TU WE TH FR SA SO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Earlier, effective due date of action
* </pre>
* <pre>
* Example 2:
* MO TU WE TH FR SA SO
* H WE WE -&gt; Holiday/WeekEnd
* X -&gt; Due date of action
* X' -&gt; Earlier, effective due date of action
* </pre>
*/
PREVIOUS_WORKDAY;
/**
* This method validates whether the given string represents a valid holiday weekend type.
*
* @param type to check
* @return whether the given type represents a valid holiday weekend type
*/
public static boolean isValidType(String type) {
return Arrays.stream(HolidayWeekendType.values()).anyMatch((holidayWeekendType) -> holidayWeekendType.name().equals(type));
}
public static List<String> valueList() {
return Arrays.stream(HolidayWeekendType.values()).map(HolidayWeekendType::name).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,36 @@
package de.financer.model;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public enum IntervalType {
/** Indicates that an action should be executed every day */
DAILY,
/** Indicates that an action should be executed once a week */
WEEKLY,
/** Indicates that an action should be executed once a month */
MONTHLY,
/** Indicates that an action should be executed once a quarter */
QUARTERLY,
/** Indicates that an action should be executed once a year */
YEARLY;
/**
* This method validates whether the given string represents a valid interval type.
*
* @param type to check
* @return whether the given type represents a valid interval type
*/
public static boolean isValidType(String type) {
return Arrays.stream(IntervalType.values()).anyMatch((intervalType) -> intervalType.name().equals(type));
}
public static List<String> valueList() {
return Arrays.stream(IntervalType.values()).map(IntervalType::name).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,83 @@
package de.financer.model;
import java.time.LocalDate;
public class RecurringTransaction {
private Long id;
private Account fromAccount;
private Account toAccount;
private String description;
private Long amount;
private IntervalType intervalType;
private LocalDate firstOccurrence;
private LocalDate lastOccurrence;
private HolidayWeekendType holidayWeekendType;
public Long getId() {
return id;
}
public Account getFromAccount() {
return fromAccount;
}
public void setFromAccount(Account fromAccount) {
this.fromAccount = fromAccount;
}
public Account getToAccount() {
return toAccount;
}
public void setToAccount(Account toAccount) {
this.toAccount = toAccount;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getAmount() {
return amount;
}
public void setAmount(Long amount) {
this.amount = amount;
}
public HolidayWeekendType getHolidayWeekendType() {
return holidayWeekendType;
}
public void setHolidayWeekendType(HolidayWeekendType holidayWeekendType) {
this.holidayWeekendType = holidayWeekendType;
}
public LocalDate getLastOccurrence() {
return lastOccurrence;
}
public void setLastOccurrence(LocalDate lastOccurrence) {
this.lastOccurrence = lastOccurrence;
}
public LocalDate getFirstOccurrence() {
return firstOccurrence;
}
public void setFirstOccurrence(LocalDate firstOccurrence) {
this.firstOccurrence = firstOccurrence;
}
public IntervalType getIntervalType() {
return intervalType;
}
public void setIntervalType(IntervalType intervalType) {
this.intervalType = intervalType;
}
}

View File

@@ -0,0 +1,65 @@
package de.financer.model;
import java.time.LocalDate;
public class Transaction {
private Long id;
private Account fromAccount;
private Account toAccount;
private LocalDate date;
private String description;
private Long amount;
private RecurringTransaction recurringTransaction;
public Long getId() {
return id;
}
public Account getFromAccount() {
return fromAccount;
}
public void setFromAccount(Account fromAccount) {
this.fromAccount = fromAccount;
}
public Account getToAccount() {
return toAccount;
}
public void setToAccount(Account toAccount) {
this.toAccount = toAccount;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getAmount() {
return amount;
}
public void setAmount(Long amount) {
this.amount = amount;
}
public RecurringTransaction getRecurringTransaction() {
return recurringTransaction;
}
public void setRecurringTransaction(RecurringTransaction recurringTransaction) {
this.recurringTransaction = recurringTransaction;
}
}

View File

@@ -0,0 +1,50 @@
package de.financer.util;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.Account;
import de.financer.model.AccountStatus;
import de.financer.model.RecurringTransaction;
import de.financer.util.comparator.AccountByTypeComparator;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.StringUtils;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
public class ControllerUtils {
public static String buildUrl(FinancerConfig financerConfig, Function function) {
return String.format("%s%s", financerConfig.getServerUrl(), function.getPath());
}
public static List<Account> filterAndSortAccounts(Iterable<Account> accounts) {
return filterAndSortAccounts(accounts, false);
}
public static List<Account> filterAndSortAccounts(Iterable<Account> accounts, boolean showClosed) {
return IterableUtils.toList(accounts).stream()
.filter((acc) -> AccountStatus.OPEN
.equals(acc.getStatus()) || showClosed)
.sorted(new AccountByTypeComparator())
.collect(Collectors.toList());
}
public static String formatDate(FinancerConfig financerConfig, String dateFromForm) {
if (StringUtils.isEmpty(dateFromForm)) {
return null;
}
// The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
final LocalDate formatted = LocalDate.parse(dateFromForm, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return formatted.format(DateTimeFormatter.ofPattern(financerConfig.getDateFormat()));
}
public static List<RecurringTransaction> filterEmptyEntries(Iterable<RecurringTransaction> recurringTransactions) {
return IterableUtils.toList(recurringTransactions).stream()
.filter((rt) -> rt.getFromAccount() != null && rt.getToAccount() != null)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,23 @@
package de.financer.util.comparator;
import de.financer.model.Account;
import de.financer.model.AccountType;
import java.util.Comparator;
import java.util.Map;
public class AccountByTypeComparator implements Comparator<Account> {
//@formatter:off
private static final Map<AccountType, Integer> SORT_ORDER = Map.of(AccountType.BANK, 1,
AccountType.CASH, 2,
AccountType.INCOME, 3,
AccountType.LIABILITY, 4,
AccountType.EXPENSE, 5,
AccountType.START, 6);
//@formatter:on
@Override
public int compare(Account o1, Account o2) {
return SORT_ORDER.get(o1.getType()).compareTo(SORT_ORDER.get(o2.getType()));
}
}

View File

@@ -0,0 +1,2 @@
# Hibernate
spring.jpa.show-sql=true

View File

@@ -0,0 +1,25 @@
###
### This is the main configuration file of the application.
### Filtering of the @...@ values happens via the maven-resource-plugin. The execution of the plugin is configured in
### the Spring Boot parent POM.
spring.profiles.active=@activeProfiles@
server.servlet.context-path=/financer-web-client
server.port=8090
info.app.name=Financer Web Client
info.app.description=A web interface for the financer server application
info.build.group=@project.groupId@
info.build.artifact=@project.artifactId@
info.build.version=@project.version@
logging.level.de.financer=DEBUG
logging.file=financer-web-client.log
# The date format of the client-supplied date string, used to parse the string into a proper object
financer.dateFormat=dd.MM.yyyy
financer.serverUrl=http://localhost:8089/financer-server/
spring.messages.basename=i18n/message

View File

@@ -0,0 +1,142 @@
financer.account-overview.title=financer\: overview
financer.account-overview.available-actions=Available actions\:
financer.account-overview.available-actions.show-closed=Show closed accounts
financer.account-overview.available-actions.hide-closed=Hide closed accounts
financer.account-overview.available-actions.create-account=Create new account
financer.account-overview.available-actions.create-transaction=Create new transaction
financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction
financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions
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\:
financer.account-overview.table-header.id=ID
financer.account-overview.table-header.key=Key
financer.account-overview.table-header.balance=Current Balance
financer.account-overview.table-header.type=Type
financer.account-overview.table-header.status=Status
financer.account-new.title=financer\: create new account
financer.account-new.label.key=Key\:
financer.account-new.label.type=Type\:
financer.account-new.submit=Create account
financer.transaction-new.title=financer\: create new transaction
financer.transaction-new.label.from-account=From account\:
financer.transaction-new.label.to-account=To account\:
financer.transaction-new.label.amount=Amount\:
financer.transaction-new.label.date=Date\:
financer.transaction-new.label.description=Description\:
financer.transaction-new.submit=Create transaction
financer.transaction-new.account-type.BANK={0}|Bank|{1}
financer.transaction-new.account-type.CASH={0}|Cash|{1}
financer.transaction-new.account-type.INCOME={0}|Income|{1}
financer.transaction-new.account-type.LIABILITY={0}|Liability|{1}
financer.transaction-new.account-type.EXPENSE={0}|Expense|{1}
financer.transaction-new.account-type.START={0}|Start|{1}
financer.recurring-transaction-new.title=financer\: create new recurring transaction
financer.recurring-transaction-new.label.from-account=From account\:
financer.recurring-transaction-new.label.to-account=To account\:
financer.recurring-transaction-new.label.amount=Amount\:
financer.recurring-transaction-new.label.first-occurrence=First occurrence\:
financer.recurring-transaction-new.label.last-occurrence=Last occurrence\:
financer.recurring-transaction-new.label.interval-type=Interval\:
financer.recurring-transaction-new.label.holiday-weekend-type=Holiday/weekend rule\:
financer.recurring-transaction-new.label.description=Description\:
financer.recurring-transaction-new.submit=Create recurring transaction
financer.recurring-transaction-new.account-type.BANK={0}|Bank|{1}
financer.recurring-transaction-new.account-type.CASH={0}|Cash|{1}
financer.recurring-transaction-new.account-type.INCOME={0}|Income|{1}
financer.recurring-transaction-new.account-type.LIABILITY={0}|Liability|{1}
financer.recurring-transaction-new.account-type.EXPENSE={0}|Expense|{1}
financer.recurring-transaction-new.account-type.START={0}|Start|{1}
financer.recurring-transaction-list.title.dueToday=financer\: recurring transactions due today
financer.recurring-transaction-list.title.active=financer\: active recurring transactions
financer.recurring-transaction-list.title.all=financer\: all recurring transaction
financer.recurring-transaction-list.table-header.id=ID
financer.recurring-transaction-list.table-header.fromAccount=From account
financer.recurring-transaction-list.table-header.toAccount=To account
financer.recurring-transaction-list.table-header.firstOccurrence=First occurrence
financer.recurring-transaction-list.table-header.lastOccurrence=Last occurrence
financer.recurring-transaction-list.table-header.amount=Amount
financer.recurring-transaction-list.table-header.description=Description
financer.recurring-transaction-list.table-header.intervalType=Interval
financer.recurring-transaction-list.table-header.holidayWeekendType=Holiday/weekend rule
financer.recurring-transaction-list.table-header.actions=Actions
financer.recurring-transaction-list.table.actions.createTransaction=Create transaction
financer.recurring-transaction-list.table.actions.createTransactionWithAmount=Create transaction with amount
financer.recurring-transaction-list.table.actions.editRecurringTransaction=Edit
financer.recurring-transaction-list.table.actions.deleteRecurringTransaction=Delete
financer.account-details.title=financer\: account details
financer.account-details.available-actions=Available actions\:
financer.account-details.available-actions.close-account=Close account
financer.account-details.available-actions.open-account=Open account
financer.account-details.table-header.id=ID
financer.account-details.table-header.fromAccount=From account
financer.account-details.table-header.toAccount=To account
financer.account-details.table-header.date=Date
financer.account-details.table-header.amount=Amount
financer.account-details.table-header.description=Description
financer.account-details.table-header.byRecurring=Recurring?
financer.account-details.details.type=Type\:
financer.account-details.details.balance=Current Balance\:
financer.account-details.table-header.actions=Actions
financer.account-details.table.actions.editTransaction=Edit
financer.account-details.table.actions.deleteTransaction=Delete
financer.interval-type.DAILY=Daily
financer.interval-type.WEEKLY=Weekly
financer.interval-type.MONTHLY=Monthly
financer.interval-type.QUARTERLY=Quarterly
financer.interval-type.YEARLY=Yearly
financer.holiday-weekend-type.SAME_DAY=Same day
financer.holiday-weekend-type.NEXT_WORKDAY=Next workday
financer.holiday-weekend-type.PREVIOUS_WORKDAY=Previous workday
financer.account-type.BANK=Bank
financer.account-type.CASH=Cash
financer.account-type.INCOME=Income
financer.account-type.LIABILITY=Liability
financer.account-type.EXPENSE=Expense
financer.account-type.START=Start
financer.account-status.OPEN=Open
financer.account-status.CLOSED=Closed
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
financer.heading.account-overview=financer\: overview
financer.heading.account-details=financer\: account details for {0}
financer.heading.recurring-transaction-list.dueToday=financer\: recurring transactions due today
financer.heading.recurring-transaction-list.active=financer\: active recurring transactions
financer.heading.recurring-transaction-list.all=financer\: all recurring transaction
financer.error-message.UNKNOWN_ERROR=An unknown error occurred!
financer.error-message.INVALID_ACCOUNT_TYPE=The selected account type is not valid!
financer.error-message.INVALID_ACCOUNT_KEY=The entered account key is invalid! It has to start with 'accounts.'.
financer.error-message.FROM_ACCOUNT_NOT_FOUND=The specified from account has not been found!
financer.error-message.TO_ACCOUNT_NOT_FOUND=The specified to account has not been found!
financer.error-message.FROM_AND_TO_ACCOUNT_NOT_FOUND=Neither from nor to have not been found!
financer.error-message.INVALID_DATE_FORMAT=The date entered has the wrong format!
financer.error-message.MISSING_DATE=No date entered!
financer.error-message.AMOUNT_ZERO=Zero is not a valid booking amount!
financer.error-message.MISSING_AMOUNT=No amount entered!
financer.error-message.INVALID_BOOKING_ACCOUNTS=The booking is not valid! No booking allowed from the from account type to the to account type!
financer.error-message.MISSING_HOLIDAY_WEEKEND_TYPE=No holiday weekend type entered!
financer.error-message.INVALID_HOLIDAY_WEEKEND_TYPE=The holiday weekend type is not valid!
financer.error-message.MISSING_INTERVAL_TYPE=No interval type entered!
financer.error-message.INVALID_INTERVAL_TYPE=The interval type is not valid!
financer.error-message.MISSING_FIRST_OCCURRENCE=No first occurrence entered!
financer.error-message.INVALID_FIRST_OCCURRENCE_FORMAT=The date format of the first occurrence is invalid!
financer.error-message.INVALID_LAST_OCCURRENCE_FORMAT=The date format of the last occurrence is invalid!
financer.error-message.MISSING_RECURRING_TRANSACTION_ID=No recurring transaction entered!
financer.error-message.INVALID_RECURRING_TRANSACTION_ID=The recurring transaction is not valid!
financer.error-message.RECURRING_TRANSACTION_NOT_FOUND=The recurring transaction could not be found!
financer.error-message.MISSING_TRANSACTION_ID=No transaction entered!
financer.error-message.INVALID_TRANSACTION_ID=The transaction is not valid!
financer.error-message.TRANSACTION_NOT_FOUND=The transaction could not be found!
financer.error-message.ACCOUNT_NOT_FOUND=The account could not be found!

View File

@@ -0,0 +1,116 @@
financer.account-overview.title=financer\: \u00DCbersicht
financer.account-overview.available-actions=Verf\u00FCgbare Aktionen\:
financer.account-overview.available-actions.show-closed=Zeige auch geschlossene Konten
financer.account-overview.available-actions.hide-closed=Verstecke geschlossene Konten
financer.account-overview.available-actions.create-account=Neues Konto erstellen
financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen
financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen
financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen
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\:
financer.account-overview.table-header.id=ID
financer.account-overview.table-header.key=Schl\u00FCssel
financer.account-overview.table-header.balance=Kontostand
financer.account-overview.table-header.type=Typ
financer.account-overview.table-header.status=Status
financer.account-new.title=financer\: Neues Konto erstellen
financer.account-new.label.key=Schl\u00FCssel\:
financer.account-new.label.type=Typ\:
financer.account-new.submit=Account erstellen
financer.transaction-new.title=financer\: Neue Buchung erstellen
financer.transaction-new.label.from-account=Von Konto\:
financer.transaction-new.label.to-account=An Konto\:
financer.transaction-new.label.amount=Betrag\:
financer.transaction-new.label.date=Datum\:
financer.transaction-new.label.description=Beschreibung\:
financer.transaction-new.submit=Buchung erstellen
financer.transaction-new.account-type.BANK={0}|Bank|{1}
financer.transaction-new.account-type.CASH={0}|Bar|{1}
financer.transaction-new.account-type.INCOME={0}|Einkommen|{1}
financer.transaction-new.account-type.LIABILITY={0}|Verbindlichkeit|{1}
financer.transaction-new.account-type.EXPENSE={0}|Aufwand|{1}
financer.transaction-new.account-type.START={0}|Anfangsbestand|{1}
financer.recurring-transaction-new.title=financer\: Neue wiederkehrende Buchung erstellen
financer.recurring-transaction-new.label.from-account=Von Konto\:
financer.recurring-transaction-new.label.to-account=An Konto\:
financer.recurring-transaction-new.label.amount=Betrag\:
financer.recurring-transaction-new.label.first-occurrence=Erstes Auftreten\:
financer.recurring-transaction-new.label.last-occurrence=Letztes Auftreten\:
financer.recurring-transaction-new.label.interval-type=Intervall\:
financer.recurring-transaction-new.label.holiday-weekend-type=Feiertag-/Wochenendregel\:
financer.recurring-transaction-new.label.description=Beschreibung\:
financer.recurring-transaction-new.submit=Wiederkehrende Buchung erstellen
financer.recurring-transaction-new.account-type.BANK={0}|Bank|{1}
financer.recurring-transaction-new.account-type.CASH={0}|Bar|{1}
financer.recurring-transaction-new.account-type.INCOME={0}|Einkommen|{1}
financer.recurring-transaction-new.account-type.LIABILITY={0}|Verbindlichkeit|{1}
financer.recurring-transaction-new.account-type.EXPENSE={0}|Aufwand|{1}
financer.recurring-transaction-new.account-type.START={0}|Anfangsbestand|{1}
financer.recurring-transaction-list.title.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig
financer.recurring-transaction-list.title.active=financer\: aktive wiederkehrende Buchungen
financer.recurring-transaction-list.title.all=financer\: alle wiederkehrende Buchungen
financer.recurring-transaction-list.table-header.id=ID
financer.recurring-transaction-list.table-header.fromAccount=Von Konto
financer.recurring-transaction-list.table-header.toAccount=An Konto
financer.recurring-transaction-list.table-header.firstOccurrence=Erstes Auftreten
financer.recurring-transaction-list.table-header.lastOccurrence=Letztes Auftreten
financer.recurring-transaction-list.table-header.amount=Betrag
financer.recurring-transaction-list.table-header.description=Beschreibung
financer.recurring-transaction-list.table-header.intervalType=Intervall
financer.recurring-transaction-list.table-header.holidayWeekendType=Feiertag-/Wochenendregel
financer.recurring-transaction-list.table-header.actions=Aktionen
financer.recurring-transaction-list.table.actions.createTransaction=Erstelle Buchung
financer.recurring-transaction-list.table.actions.createTransactionWithAmount=Erstelle Buchung mit Betrag
financer.recurring-transaction-list.table.actions.editRecurringTransaction=Bearbeiten
financer.recurring-transaction-list.table.actions.deleteRecurringTransaction=Löschen
financer.account-details.title=financer\: Kontodetails
financer.account-details.available-actions=Verf\u00FCgbare Aktionen\:
financer.account-details.available-actions.close-account=Konto schlie\u00DFen
financer.account-details.available-actions.open-account=Konto \u00F6ffnen
financer.account-details.table-header.id=ID
financer.account-details.table-header.fromAccount=Von Konto
financer.account-details.table-header.toAccount=An Konto
financer.account-details.table-header.date=Datum
financer.account-details.table-header.amount=Betrag
financer.account-details.table-header.description=Beschreibung
financer.account-details.table-header.byRecurring=Wiederkehrend?
financer.account-details.details.type=Typ\:
financer.account-details.details.balance=Kontostand\:
financer.account-details.table-header.actions=Aktionen
financer.account-details.table.actions.editTransaction=Bearbeiten
financer.account-details.table.actions.deleteTransaction=Löschen
financer.interval-type.DAILY=T\u00E4glich
financer.interval-type.WEEKLY=W\u00F6chentlich
financer.interval-type.MONTHLY=Monatlich
financer.interval-type.QUARTERLY=Viertelj\u00E4hrlich
financer.interval-type.YEARLY=J\u00E4hrlich
financer.holiday-weekend-type.SAME_DAY=Gleicher Tag
financer.holiday-weekend-type.NEXT_WORKDAY=N\u00E4chster Werktag
financer.holiday-weekend-type.PREVIOUS_WORKDAY=Vorheriger Werktag
financer.account-type.BANK=Bank
financer.account-type.CASH=Bar
financer.account-type.INCOME=Einkommen
financer.account-type.LIABILITY=Verbindlichkeit
financer.account-type.EXPENSE=Aufwand
financer.account-type.START=Anfangsbestand
financer.account-status.OPEN=Offen
financer.account-status.CLOSED=Geschlossen
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
financer.heading.account-overview=financer\: \u00DCbersicht
financer.heading.account-details=financer\: Kontodetails f\u00FCr {0}
financer.heading.recurring-transaction-list.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig
financer.heading.recurring-transaction-list.active=financer\: aktive wiederkehrende Buchungen
financer.heading.recurring-transaction-list.all=financer\: alle wiederkehrende Buchungen

View File

@@ -0,0 +1,67 @@
#account-overview-table,
#account-transaction-table,
#recurring-transaction-list-table {
width: 100%;
border-collapse: collapse;
text-align: left;
margin-top: 1.5em;
margin-bottom: 1.5em;
}
#account-overview-table th,
#account-overview-table td,
#account-transaction-table th,
#account-transaction-table td,
#recurring-transaction-list-table th,
#recurring-transaction-list-table td {
border-bottom: 1px solid #ddd;
padding: 0.3em;
vertical-align: top;
}
tr:hover {
background-color: lightgrey;
}
@media only screen and (max-width: 450px) {
.hideable-column {
display: none;
}
#new-account-form *,
#new-transaction-form *,
#new-recurring-transaction-form * {
width: 100% !important;
}
}
#action-container *,
#recurring-transaction-list-table-actions-container *,
#account-transaction-table-actions-container * {
display: block;
}
#details-container,
#status-container {
margin-bottom: 1em;
}
#status-container > span, div {
display: block;
}
#details-container > div {
display: block;
}
#new-account-form *,
#new-transaction-form *,
#new-recurring-transaction-form * {
display: block;
margin-top: 1em;
width: 20em;
box-sizing: border-box;
}
.errorMessage {
color: #ff6666
}

View File

@@ -0,0 +1,59 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.account-details.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
</head>
<body>
<h1 th:text="#{financer.heading.account-details(${account.key})}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<div id="details-container">
<div id="type-container">
<span th:text="#{financer.account-details.details.type}"/>
<span th:text="#{'financer.account-type.' + ${account.type}}"/>
</div>
<div id="balance-container">
<span th:text="#{financer.account-details.details.balance}"/>
<span th:text="${#numbers.formatDecimal(account.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/>
</div>
</div>
<div id="action-container">
<span th:text="#{financer.account-details.available-actions}"/>
<a th:if="${!isClosed}" th:href="@{/closeAccount(key=${account.key})}"
th:text="#{financer.account-details.available-actions.close-account}"/>
<a th:if="${isClosed}" th:href="@{/openAccount(key=${account.key})}"
th:text="#{financer.account-details.available-actions.open-account}"/>
</div>
<table id="account-transaction-table">
<tr>
<th class="hideable-column" th:text="#{financer.account-details.table-header.id}"/>
<th th:text="#{financer.account-details.table-header.fromAccount}"/>
<th th:text="#{financer.account-details.table-header.toAccount}"/>
<th th:text="#{financer.account-details.table-header.date}"/>
<th th:text="#{financer.account-details.table-header.amount}"/>
<th th:text="#{financer.account-details.table-header.description}"/>
<th th:text="#{financer.account-details.table-header.byRecurring}"/>
<th th:text="#{financer.account-details.table-header.actions}"/>
</tr>
<tr th:each="transaction : ${transactions}">
<td class="hideable-column" th:text="${transaction.id}"/>
<td th:text="${transaction.fromAccount.key}" />
<td th:text="${transaction.toAccount.key}" />
<td th:text="${transaction.date}" />
<td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/>
<td th:text="${transaction.description}" />
<td th:text="${transaction.recurringTransaction != null}" />
<td>
<div id="account-transaction-table-actions-container">
<a th:href="@{/editTransaction}"
th:text="#{financer.account-details.table.actions.editTransaction}"/>
<a th:href="@{/deleteTransaction}"
th:text="#{financer.account-details.table.actions.deleteTransaction}"/>
</div>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,54 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.account-overview.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
</head>
<body>
<h1 th:text="#{financer.heading.account-overview}" />
<div id="status-container">
<span th:text="#{financer.account-overview.status}"/>
<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 id="action-container">
<span th:text="#{financer.account-overview.available-actions}"/>
<a th:if="${!showClosed}" th:href="@{'/accountOverview?showClosed=true'}"
th:text="#{financer.account-overview.available-actions.show-closed}"/>
<a th:if="${showClosed}" th:href="@{'/accountOverview'}"
th:text="#{financer.account-overview.available-actions.hide-closed}"/>
<a th:href="@{/newAccount}" th:text="#{financer.account-overview.available-actions.create-account}"/>
<a th:href="@{/newTransaction}" th:text="#{financer.account-overview.available-actions.create-transaction}"/>
<a th:href="@{/newRecurringTransaction}"
th:text="#{financer.account-overview.available-actions.create-recurring-transaction}"/>
<a th:href="@{/recurringTransactionAll}"
th:text="#{financer.account-overview.available-actions.recurring-transaction-all}"/>
</div>
<table id="account-overview-table">
<tr>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.id}"/>
<th th:text="#{financer.account-overview.table-header.key}"/>
<th th:text="#{financer.account-overview.table-header.balance}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.type}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.status}"/>
</tr>
<tr th:each="acc : ${accounts}">
<td class="hideable-column" th:text="${acc.id}"/>
<td>
<a th:href="@{/accountDetails(key=${acc.key})}" th:text="${acc.key}"/>
</td>
<td th:text="${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/>
<td class="hideable-column" th:text="#{'financer.account-type.' + ${acc.type}}"/>
<td class="hideable-column" th:text="#{'financer.account-status.' + ${acc.status}}"/>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.account-new.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
</head>
<body>
<h1 th:text="#{financer.heading.account-new}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<form id="new-account-form" action="#" th:action="@{/saveAccount}" th:object="${newAccountForm}" method="post">
<label for="inputKey" th:text="#{financer.account-new.label.key}"/>
<input type="text" id="inputKey" th:field="*{key}" placeholder="accounts."/>
<label for="selectType" th:text="#{financer.account-new.label.type}"/>
<select id="selectType" th:field="*{type}">
<option th:each="type : ${accounttypes}" th:value="${type}" th:text="#{'financer.account-type.' + ${type}}"/>
</select>
<input type="submit" th:value="#{financer.account-new.submit}" />
</form>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.recurring-transaction-new.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
</head>
<body>
<h1 th:text="#{financer.heading.recurring-transaction-new}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<form id="new-recurring-transaction-form" action="#" th:action="@{/saveRecurringTransaction}" th:object="${newRecurringTransactionForm}"
method="post">
<label for="selectFromAccount" th:text="#{financer.recurring-transaction-new.label.from-account}"/>
<select id="selectFromAccount" th:field="*{fromAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
th:text="#{'financer.recurring-transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/>
</select>
<label for="selectToAccount" th:text="#{financer.recurring-transaction-new.label.to-account}"/>
<select id="selectToAccount" th:field="*{toAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
th:text="#{'financer.recurring-transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/>
</select>
<label for="inputAmount" th:text="#{financer.recurring-transaction-new.label.amount}"/>
<input type="text" id="inputAmount" th:field="*{amount}"/>
<label for="inputFirstOccurrence" th:text="#{financer.recurring-transaction-new.label.first-occurrence}"/>
<input type="date" id="inputFirstOccurrence" th:field="*{firstOccurrence}"/>
<label for="inputLastOccurrence" th:text="#{financer.recurring-transaction-new.label.last-occurrence}"/>
<input type="date" id="inputLastOccurrence" th:field="*{lastOccurrence}"/>
<label for="selectInterval" th:text="#{financer.recurring-transaction-new.label.interval-type}"/>
<select id="selectInterval" th:field="*{intervalType}">
<option th:each="inter : ${intervalTypes}" th:value="${inter}"
th:text="#{'financer.interval-type.' + ${inter}}"/>
</select>
<label for="selectHolidayWeekend" th:text="#{financer.recurring-transaction-new.label.holiday-weekend-type}"/>
<select id="selectHolidayWeekend" th:field="*{holidayWeekendType}">
<option th:each="hdwt : ${holidayWeekendTypes}" th:value="${hdwt}"
th:text="#{'financer.holiday-weekend-type.' + ${hdwt}}"/>
</select>
<label for="inputDescription" th:text="#{financer.recurring-transaction-new.label.description}"/>
<input type="text" id="inputDescription" th:field="*{description}"/>
<input type="submit" th:value="#{financer.recurring-transaction-new.submit}"/>
</form>
</body>
</html>

View File

@@ -0,0 +1,53 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{'financer.recurring-transaction-list.title.' + ${subTitle}}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
</head>
<body>
<h1 th:text="#{'financer.heading.recurring-transaction-list.' + ${subTitle}}"/>
<table id="recurring-transaction-list-table">
<tr>
<th class="hideable-column" th:text="#{financer.recurring-transaction-list.table-header.id}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.fromAccount}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.toAccount}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.firstOccurrence}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.lastOccurrence}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.amount}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.description}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.intervalType}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.holidayWeekendType}"/>
<th th:text="#{financer.recurring-transaction-list.table-header.actions}"/>
</tr>
<tr th:each="rt : ${recurringTransactions}">
<td class="hideable-column" th:text="${rt.id}"/>
<td>
<a th:href="@{/accountDetails(key=${rt.fromAccount.key})}" th:text="${rt.fromAccount.key}"/>
</td>
<td>
<a th:href="@{/accountDetails(key=${rt.toAccount.key})}" th:text="${rt.toAccount.key}"/>
</td>
<td th:text="${rt.firstOccurrence}"/>
<td th:text="${rt.lastOccurrence}"/>
<td th:text="${#numbers.formatDecimal(rt.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/>
<td th:text="${rt.description}"/>
<td th:text="${rt.intervalType}"/>
<td th:text="${rt.holidayWeekendType}"/>
<td>
<div id="recurring-transaction-list-table-actions-container">
<a th:href="@{/recurringToTransaction}"
th:text="#{financer.recurring-transaction-list.table.actions.createTransaction}"/>
<a th:href="@{/recurringToTransactionWithAmount}"
th:text="#{financer.recurring-transaction-list.table.actions.createTransactionWithAmount}"/>
<a th:href="@{/editRecurringTransaction}"
th:text="#{financer.recurring-transaction-list.table.actions.editRecurringTransaction}"/>
<a th:href="@{/deleteRecurringTransaction}"
th:text="#{financer.recurring-transaction-list.table.actions.deleteRecurringTransaction}"/>
</div>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.transaction-new.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
</head>
<body>
<h1 th:text="#{financer.heading.transaction-new}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<form id="new-transaction-form" action="#" th:action="@{/saveTransaction}" th:object="${newTransactionForm}"
method="post">
<label for="selectFromAccount" th:text="#{financer.transaction-new.label.from-account}"/>
<select id="selectFromAccount" th:field="*{fromAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
th:text="#{'financer.transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/>
</select>
<label for="selectToAccount" th:text="#{financer.transaction-new.label.to-account}"/>
<select id="selectToAccount" th:field="*{toAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
th:text="#{'financer.transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/>
</select>
<label for="inputAmount" th:text="#{financer.transaction-new.label.amount}"/>
<input type="text" id="inputAmount" th:field="*{amount}"/>
<label for="inputDate" th:text="#{financer.transaction-new.label.date}"/>
<input type="date" id="inputDate" th:field="*{date}"/>
<label for="inputDescription" th:text="#{financer.transaction-new.label.description}"/>
<input type="text" id="inputDescription" th:field="*{description}"/>
<input type="submit" th:value="#{financer.transaction-new.submit}"/>
</form>
</body>
</html>

View File

@@ -0,0 +1,31 @@
package de.financer;
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.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FinancerApplication.class)
@AutoConfigureMockMvc
@TestPropertySource(
locations = "classpath:application-integrationtest.properties")
public class FinancerApplicationBootTest {
@Autowired
private MockMvc mockMvc;
@Test
public void test_appBoots() {
// Nothing to do in this test as we just want to startup the app
// to make sure that spring, flyway and hibernate all work
// as expected even after changes
// While this slightly increases build time it's an easy and safe
// way to ensure that the app can start
Assert.assertTrue(true);
}
}

View File

@@ -0,0 +1,5 @@
spring.profiles.active=dev
spring.datasource.url=jdbc:hsqldb:mem:.
spring.datasource.username=sa
spring.flyway.locations=classpath:/database/hsqldb,classpath:/database/hsqldb/integration,classpath:/database/common