diff --git a/financer-web-client/.gitignore b/financer-web-client/.gitignore
new file mode 100644
index 0000000..007ac1c
--- /dev/null
+++ b/financer-web-client/.gitignore
@@ -0,0 +1,4 @@
+financer-web-client.log*
+.attach*
+financer-server.log*
+*.iml
\ No newline at end of file
diff --git a/financer-web-client/pom.xml b/financer-web-client/pom.xml
new file mode 100644
index 0000000..6b810ca
--- /dev/null
+++ b/financer-web-client/pom.xml
@@ -0,0 +1,93 @@
+
+
+
+ 4.0.0
+
+
+ de.77zzcx7.financer
+ financer-parent
+ 9-SNAPSHOT
+ ../financer-parent
+
+
+ de.77zzcx7.financer
+ financer-web-client
+ ${packaging.type}
+ The web client part of the financer application - a simple app to manage your personal finances
+ financer-web-client
+
+
+ jar
+ dev
+
+ ${project.artifactId}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+ org.apache.commons
+ commons-collections4
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+
+ junit
+ junit
+ test
+
+
+
+
+
+ build-war
+
+ war
+ postgres
+ financer
+
+
+ ${finalName}##${parallelDeploymentVersion}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-tomcat
+ provided
+
+
+
+
+
diff --git a/financer-web-client/src/main/java/de/financer/FinancerApplication.java b/financer-web-client/src/main/java/de/financer/FinancerApplication.java
new file mode 100644
index 0000000..8737a0b
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/FinancerApplication.java
@@ -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);
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/ResponseReason.java b/financer-web-client/src/main/java/de/financer/ResponseReason.java
new file mode 100644
index 0000000..f73464b
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/ResponseReason.java
@@ -0,0 +1,55 @@
+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),
+ 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),
+ DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
+ DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
+ ACCOUNT_GROUP_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 entity) {
+ for (ResponseReason reason : values()) {
+ if (reason.name().equals(entity.getBody())) {
+ return reason;
+ }
+ }
+
+ return UNKNOWN_ERROR;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/config/FinancerConfig.java b/financer-web-client/src/main/java/de/financer/config/FinancerConfig.java
new file mode 100644
index 0000000..26438a0
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/config/FinancerConfig.java
@@ -0,0 +1,40 @@
+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;
+ private String version;
+
+ 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;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/AccountController.java b/financer-web-client/src/main/java/de/financer/controller/AccountController.java
new file mode 100644
index 0000000..8c69a8f
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/AccountController.java
@@ -0,0 +1,171 @@
+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 de.financer.util.TransactionUtils;
+import de.financer.util.comparator.TransactionByDateByIdDescComparator;
+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;
+
+import java.util.List;
+
+@Controller
+public class AccountController {
+ @Autowired
+ private FinancerConfig financerConfig;
+
+ @GetMapping({"/", "/accountOverview"})
+ public String accountOverview(String showClosed, Model model) {
+ final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
+ final ResponseEntity> rtDtRes = new GetAllRecurringTransactionsDueTodayTemplate()
+ .exchange(this.financerConfig);
+ final ResponseEntity> 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);
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "account/accountOverview";
+ }
+
+ @GetMapping("/newAccount")
+ public String newAccount(Model model) {
+ final ResponseEntity> accountGroupResponse = new GetAllAccountGroupsTemplate()
+ .exchange(this.financerConfig);
+
+ model.addAttribute("accountGroups", ControllerUtils.sortAccountGroups(accountGroupResponse.getBody()));
+ model.addAttribute("accountTypes", AccountType.valueList());
+ model.addAttribute("form", new NewAccountForm());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "account/newAccount";
+ }
+
+ @PostMapping("/saveAccount")
+ public String saveAccont(NewAccountForm form, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT))
+ .queryParam("key", form.getKey())
+ .queryParam("accountGroupName", form.getGroup())
+ .queryParam("type", form.getType());
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ final ResponseEntity> accountGroupResponse = new GetAllAccountGroupsTemplate()
+ .exchange(this.financerConfig);
+
+ model.addAttribute("form", form);
+ model.addAttribute("accountGroups", ControllerUtils.sortAccountGroups(accountGroupResponse.getBody()));
+ model.addAttribute("accountTypes", AccountType.valueList());
+ model.addAttribute("errorMessage", responseReason.name());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "account/newAccount";
+ }
+
+ return "redirect:/accountOverview";
+ }
+
+ @GetMapping("/accountDetails")
+ public String accountDetails(String key, Model model) {
+ final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
+ final Account account = response.getBody();
+ final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate()
+ .exchange(this.financerConfig, account.getKey());
+
+ List transactions = IterableUtils.toList(transactionResponse.getBody());
+
+ transactions.sort(new TransactionByDateByIdDescComparator());
+ transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
+
+ model.addAttribute("account", account);
+ model.addAttribute("transactions", transactions);
+ model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ 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 closeResponse = new StringTemplate().exchange(closeBuilder);
+
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
+ final Account account = response.getBody();
+ final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate()
+ .exchange(this.financerConfig, account.getKey());
+
+ List transactions = IterableUtils.toList(transactionResponse.getBody());
+
+ transactions.sort(new TransactionByDateByIdDescComparator());
+ transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
+
+ model.addAttribute("account", account);
+ model.addAttribute("transactions", transactions);
+ model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
+ model.addAttribute("errorMessage", responseReason.name());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ 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 closeResponse = new StringTemplate().exchange(openBuilder);
+
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
+ final Account account = response.getBody();
+ final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate()
+ .exchange(this.financerConfig, account.getKey());
+
+ List transactions = IterableUtils.toList(transactionResponse.getBody());
+
+ transactions.sort(new TransactionByDateByIdDescComparator());
+ transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
+
+ model.addAttribute("account", account);
+ model.addAttribute("transactions", transactions);
+ model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
+ model.addAttribute("errorMessage", responseReason.name());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "account/accountDetails";
+ }
+
+ return "redirect:/accountOverview";
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/AccountGroupController.java b/financer-web-client/src/main/java/de/financer/controller/AccountGroupController.java
new file mode 100644
index 0000000..047d330
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/AccountGroupController.java
@@ -0,0 +1,48 @@
+package de.financer.controller;
+
+import de.financer.ResponseReason;
+import de.financer.config.FinancerConfig;
+import de.financer.controller.template.StringTemplate;
+import de.financer.form.NewAccountGroupForm;
+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 AccountGroupController {
+ @Autowired
+ private FinancerConfig financerConfig;
+
+ @GetMapping("/newAccountGroup")
+ public String newAccountGroup(Model model) {
+ model.addAttribute("form", new NewAccountGroupForm());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "accountGroup/newAccountGroup";
+ }
+
+ @PostMapping("/saveAccountGroup")
+ public String saveAccontGroup(NewAccountGroupForm form, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_GP_CREATE_ACCOUNT_GROUP))
+ .queryParam("name", form.getName());
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ model.addAttribute("form", form);
+ model.addAttribute("errorMessage", responseReason.name());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "accountGroup/newAccountGroup";
+ }
+
+ return "redirect:/accountOverview";
+ }
+}
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
new file mode 100644
index 0000000..602a4fd
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/Function.java
@@ -0,0 +1,35 @@
+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"),
+
+ ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"),
+ ACC_GP_GET_ALL("accountGroups/getAll"),
+
+ 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;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java b/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java
new file mode 100644
index 0000000..9819489
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/RecurringTransactionController.java
@@ -0,0 +1,237 @@
+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.form.RecurringToTransactionWithAmountForm;
+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;
+
+import java.util.ArrayList;
+
+@Controller
+public class RecurringTransactionController {
+
+ @Autowired
+ private FinancerConfig financerConfig;
+
+ @GetMapping("/newRecurringTransaction")
+ public String newRecurringTransaction(Model model) {
+ final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
+ final NewRecurringTransactionForm form = new NewRecurringTransactionForm();
+
+ form.setRemind(Boolean.TRUE);
+
+ model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody()));
+ model.addAttribute("intervalTypes", IntervalType.valueList());
+ model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList());
+ model.addAttribute("form", form);
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "recurringTransaction/newRecurringTransaction";
+ }
+
+ @PostMapping("/saveRecurringTransaction")
+ public String saveRecurringTransaction(NewRecurringTransactionForm form, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.RT_CREATE_RECURRING_TRANSACTION))
+ .queryParam("fromAccountKey", form.getFromAccountKey())
+ .queryParam("toAccountKey", form.getToAccountKey())
+ .queryParam("amount", form.getAmount())
+ .queryParam("firstOccurrence", ControllerUtils.formatDate(this.financerConfig, form.getFirstOccurrence()))
+ .queryParam("lastOccurrence", ControllerUtils.formatDate(this.financerConfig, form.getLastOccurrence()))
+ .queryParam("holidayWeekendType", form.getHolidayWeekendType())
+ .queryParam("intervalType", form.getIntervalType())
+ .queryParam("description", form.getDescription())
+ .queryParam("remind", form.getRemind());
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ final ResponseEntity> 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", form);
+ model.addAttribute("errorMessage", responseReason.name());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "recurringTransaction/newRecurringTransaction";
+ }
+
+ return "redirect:/accountOverview";
+ }
+
+ @GetMapping("/recurringTransactionDueToday")
+ public String recurringTransactionDueToday(Model model) {
+ final ResponseEntity> response = new GetAllRecurringTransactionsDueTodayTemplate()
+ .exchange(this.financerConfig);
+
+ model.addAttribute("recurringTransactions", response.getBody());
+ model.addAttribute("subTitle", "dueToday");
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ @GetMapping("/recurringTransactionActive")
+ public String recurringTransactionActive(Model model) {
+ final ResponseEntity> response = new GetAllActiveRecurringTransactionsTemplate()
+ .exchange(this.financerConfig);
+
+ model.addAttribute("recurringTransactions", response.getBody());
+ model.addAttribute("subTitle", "active");
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ @GetMapping("/recurringTransactionAll")
+ public String recurringTransactionAll(Model model) {
+ final ResponseEntity> response = new GetAllRecurringTransactionsTemplate()
+ .exchange(this.financerConfig);
+
+ model.addAttribute("recurringTransactions", response.getBody());
+ model.addAttribute("subTitle", "all");
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ @GetMapping("/deleteRecurringTransaction")
+ public String deleteRecurringTransaction(String recurringTransactionId, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.RT_DELETE_RECURRING_TRANSACTION))
+ .queryParam("recurringTransactionId", recurringTransactionId);
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+ final ResponseEntity> allResponse = new GetAllRecurringTransactionsTemplate()
+ .exchange(this.financerConfig);
+
+ model.addAttribute("recurringTransactions", allResponse.getBody());
+ model.addAttribute("subTitle", "all");
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ model.addAttribute("errorMessage", responseReason.name());
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ @GetMapping("/recurringToTransaction")
+ public String recurringToTransaction(String recurringTransactionId, String sub, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.RT_CREATE_TRANSACTION))
+ .queryParam("recurringTransactionId", recurringTransactionId);
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ model.addAttribute("errorMessage", responseReason.name());
+ model.addAttribute("subTitle", sub);
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ Iterable recurringTransactions;
+
+ switch (sub) {
+ case "all":
+ recurringTransactions = new GetAllRecurringTransactionsTemplate()
+ .exchange(this.financerConfig).getBody();
+ break;
+ case "active":
+ recurringTransactions = new GetAllRecurringTransactionsTemplate()
+ .exchange(this.financerConfig).getBody();
+ break;
+ case "dueToday":
+ recurringTransactions = new GetAllRecurringTransactionsDueTodayTemplate()
+ .exchange(this.financerConfig).getBody();
+ break;
+ default:
+ recurringTransactions = new ArrayList<>();
+ }
+
+ model.addAttribute("recurringTransactions", recurringTransactions);
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ return "redirect:/accountOverview";
+ }
+
+ @GetMapping("/recurringToTransactionWithAmount")
+ public String recurringToTransactionWithAmount(String recurringTransactionId, String sub, Model model) {
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ RecurringToTransactionWithAmountForm form = new RecurringToTransactionWithAmountForm();
+
+ form.setRecurringTransactionId(recurringTransactionId);
+ form.setSubTitle(sub);
+
+ model.addAttribute("form", form);
+
+ return "recurringTransaction/recurringToTransactionWithAmount";
+ }
+
+ @PostMapping("/createTransactionWithAmount")
+ public String createTransactionWithAmount(RecurringToTransactionWithAmountForm form, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.RT_CREATE_TRANSACTION))
+ .queryParam("recurringTransactionId", form.getRecurringTransactionId())
+ .queryParam("amount", form.getAmount());
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ model.addAttribute("errorMessage", responseReason.name());
+ model.addAttribute("subTitle", form.getSubTitle());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ Iterable recurringTransactions;
+
+ switch (form.getSubTitle()) {
+ case "all":
+ recurringTransactions = new GetAllRecurringTransactionsTemplate()
+ .exchange(this.financerConfig).getBody();
+ break;
+ case "active":
+ recurringTransactions = new GetAllRecurringTransactionsTemplate()
+ .exchange(this.financerConfig).getBody();
+ break;
+ case "dueToday":
+ recurringTransactions = new GetAllRecurringTransactionsDueTodayTemplate()
+ .exchange(this.financerConfig).getBody();
+ break;
+ default:
+ recurringTransactions = new ArrayList<>();
+ }
+
+ model.addAttribute("recurringTransactions", recurringTransactions);
+
+ return "recurringTransaction/recurringTransactionList";
+ }
+
+ return "redirect:/accountOverview";
+ }
+
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/TransactionController.java b/financer-web-client/src/main/java/de/financer/controller/TransactionController.java
new file mode 100644
index 0000000..da0eb95
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/TransactionController.java
@@ -0,0 +1,103 @@
+package de.financer.controller;
+
+import de.financer.ResponseReason;
+import de.financer.config.FinancerConfig;
+import de.financer.controller.template.GetAccountByKeyTemplate;
+import de.financer.controller.template.GetAllAccountsTemplate;
+import de.financer.controller.template.GetAllTransactionsForAccountTemplate;
+import de.financer.controller.template.StringTemplate;
+import de.financer.form.NewTransactionForm;
+import de.financer.model.Account;
+import de.financer.model.AccountStatus;
+import de.financer.model.Transaction;
+import de.financer.util.ControllerUtils;
+import de.financer.util.TransactionUtils;
+import de.financer.util.comparator.TransactionByDateByIdDescComparator;
+import org.apache.commons.collections4.IterableUtils;
+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;
+
+import java.util.List;
+
+@Controller
+public class TransactionController {
+
+ @Autowired
+ private FinancerConfig financerConfig;
+
+ @GetMapping("/newTransaction")
+ public String newTransaction(Model model) {
+ final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
+
+ model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody()));
+ model.addAttribute("form", new NewTransactionForm());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "transaction/newTransaction";
+ }
+
+ @PostMapping("/saveTransaction")
+ public String saveTransaction(NewTransactionForm form, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_CREATE_TRANSACTION))
+ .queryParam("fromAccountKey", form.getFromAccountKey())
+ .queryParam("toAccountKey", form.getToAccountKey())
+ .queryParam("amount", form.getAmount())
+ .queryParam("date", ControllerUtils.formatDate(this.financerConfig, form.getDate()))
+ .queryParam("description", form.getDescription());
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ final ResponseEntity> accRes = new GetAllAccountsTemplate().exchange(this.financerConfig);
+
+ model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(accRes.getBody()));
+ model.addAttribute("form", form);
+ model.addAttribute("errorMessage", responseReason.name());
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ return "transaction/newTransaction";
+ }
+
+ return "redirect:/accountOverview";
+ }
+
+ @GetMapping("/deleteTransaction")
+ public String deleteTransaction(String transactionId, String accountKey, Model model) {
+ final UriComponentsBuilder builder = UriComponentsBuilder
+ .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_DELETE_TRANSACTION))
+ .queryParam("transactionId", transactionId);
+
+ final ResponseEntity response = new StringTemplate().exchange(builder);
+ final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
+
+ final ResponseEntity accountResponse = new GetAccountByKeyTemplate().exchange(this.financerConfig, accountKey);
+ final Account account = accountResponse.getBody();
+ final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate()
+ .exchange(this.financerConfig, account.getKey());
+
+ List transactions = IterableUtils.toList(transactionResponse.getBody());
+
+ transactions.sort(new TransactionByDateByIdDescComparator());
+ transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
+
+ model.addAttribute("account", account);
+ model.addAttribute("transactions", transactions);
+ model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
+ ControllerUtils.addVersionAttribute(model, this.financerConfig);
+
+ if (!ResponseReason.OK.equals(responseReason)) {
+ model.addAttribute("errorMessage", responseReason.name());
+
+ return "account/accountDetails";
+ }
+
+ return "account/accountDetails";
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java b/financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java
new file mode 100644
index 0000000..16c6eaf
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java
@@ -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 {
+
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/FinancerRestTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/FinancerRestTemplate.java
new file mode 100644
index 0000000..bd4d604
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/FinancerRestTemplate.java
@@ -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 {
+ public ResponseEntity exchange(String url, ParameterizedTypeReference type) {
+ final RestTemplate restTemplate = new RestTemplate();
+
+ restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
+
+ return restTemplate.exchange(url, HttpMethod.GET, null, type);
+ }
+
+ public ResponseEntity exchange(String url, Class 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;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAccountByKeyTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAccountByKeyTemplate.java
new file mode 100644
index 0000000..d2552b4
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAccountByKeyTemplate.java
@@ -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 exchange(FinancerConfig financerConfig, String key) {
+ final UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(ControllerUtils
+ .buildUrl(financerConfig, Function.ACC_GET_BY_KEY))
+ .queryParam("key", key);
+
+ return new FinancerRestTemplate()
+ .exchange(builder.toUriString(), new ParameterizedTypeReference() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAllAccountGroupsTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAllAccountGroupsTemplate.java
new file mode 100644
index 0000000..46b2ecc
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAllAccountGroupsTemplate.java
@@ -0,0 +1,16 @@
+package de.financer.controller.template;
+
+import de.financer.config.FinancerConfig;
+import de.financer.controller.Function;
+import de.financer.model.AccountGroup;
+import de.financer.util.ControllerUtils;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.ResponseEntity;
+
+public class GetAllAccountGroupsTemplate {
+ public ResponseEntity> exchange(FinancerConfig financerConfig) {
+ return new FinancerRestTemplate>().exchange(ControllerUtils
+ .buildUrl(financerConfig, Function.ACC_GP_GET_ALL), new ParameterizedTypeReference>() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAllAccountsTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAllAccountsTemplate.java
new file mode 100644
index 0000000..b1933b5
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAllAccountsTemplate.java
@@ -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> exchange(FinancerConfig financerConfig) {
+ return new FinancerRestTemplate>().exchange(ControllerUtils
+ .buildUrl(financerConfig, Function.ACC_GET_ALL), new ParameterizedTypeReference>() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAllActiveRecurringTransactionsTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAllActiveRecurringTransactionsTemplate.java
new file mode 100644
index 0000000..8d779b6
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAllActiveRecurringTransactionsTemplate.java
@@ -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> exchange(FinancerConfig financerConfig) {
+ return new FinancerRestTemplate>().exchange(ControllerUtils
+ .buildUrl(financerConfig, Function.RT_GET_ALL_ACTIVE), new ParameterizedTypeReference>() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsDueTodayTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsDueTodayTemplate.java
new file mode 100644
index 0000000..a63fa03
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsDueTodayTemplate.java
@@ -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> exchange(FinancerConfig financerConfig) {
+ return new FinancerRestTemplate>().exchange(ControllerUtils
+ .buildUrl(financerConfig, Function.RT_GET_ALL_DUE_TODAY), new ParameterizedTypeReference>() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsTemplate.java
new file mode 100644
index 0000000..071bb51
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsTemplate.java
@@ -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> exchange(FinancerConfig financerConfig) {
+ return new FinancerRestTemplate>().exchange(ControllerUtils
+ .buildUrl(financerConfig, Function.RT_GET_ALL), new ParameterizedTypeReference>() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/GetAllTransactionsForAccountTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/GetAllTransactionsForAccountTemplate.java
new file mode 100644
index 0000000..8f3b4de
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/GetAllTransactionsForAccountTemplate.java
@@ -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> 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>()
+ .exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference>() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/controller/template/StringTemplate.java b/financer-web-client/src/main/java/de/financer/controller/template/StringTemplate.java
new file mode 100644
index 0000000..96148e0
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/controller/template/StringTemplate.java
@@ -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 exchange(UriComponentsBuilder builder) {
+ return new FinancerRestTemplate()
+ .exchange(builder.toUriString(), new ParameterizedTypeReference() {
+ });
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/form/NewAccountForm.java b/financer-web-client/src/main/java/de/financer/form/NewAccountForm.java
new file mode 100644
index 0000000..dea8a7d
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/form/NewAccountForm.java
@@ -0,0 +1,31 @@
+package de.financer.form;
+
+public class NewAccountForm {
+ private String key;
+ private String type;
+ private String group;
+
+ 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;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String group) {
+ this.group = group;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/form/NewAccountGroupForm.java b/financer-web-client/src/main/java/de/financer/form/NewAccountGroupForm.java
new file mode 100644
index 0000000..4872398
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/form/NewAccountGroupForm.java
@@ -0,0 +1,13 @@
+package de.financer.form;
+
+public class NewAccountGroupForm {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/form/NewRecurringTransactionForm.java b/financer-web-client/src/main/java/de/financer/form/NewRecurringTransactionForm.java
new file mode 100644
index 0000000..a077eb0
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/form/NewRecurringTransactionForm.java
@@ -0,0 +1,85 @@
+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;
+ private Boolean remind;
+
+ 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;
+ }
+
+ public Boolean getRemind() {
+ return remind;
+ }
+
+ public void setRemind(Boolean remind) {
+ this.remind = remind;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/form/NewTransactionForm.java b/financer-web-client/src/main/java/de/financer/form/NewTransactionForm.java
new file mode 100644
index 0000000..89dcd6b
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/form/NewTransactionForm.java
@@ -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;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/form/RecurringToTransactionWithAmountForm.java b/financer-web-client/src/main/java/de/financer/form/RecurringToTransactionWithAmountForm.java
new file mode 100644
index 0000000..44c3e79
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/form/RecurringToTransactionWithAmountForm.java
@@ -0,0 +1,31 @@
+package de.financer.form;
+
+public class RecurringToTransactionWithAmountForm {
+ private String recurringTransactionId;
+ private String amount;
+ private String subTitle;
+
+ public String getRecurringTransactionId() {
+ return recurringTransactionId;
+ }
+
+ public void setRecurringTransactionId(String recurringTransactionId) {
+ this.recurringTransactionId = recurringTransactionId;
+ }
+
+ public String getAmount() {
+ return amount;
+ }
+
+ public void setAmount(String amount) {
+ this.amount = amount;
+ }
+
+ public String getSubTitle() {
+ return subTitle;
+ }
+
+ public void setSubTitle(String subTitle) {
+ this.subTitle = subTitle;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/Account.java b/financer-web-client/src/main/java/de/financer/model/Account.java
new file mode 100644
index 0000000..50d0ff3
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/Account.java
@@ -0,0 +1,54 @@
+package de.financer.model;
+
+public class Account {
+ private Long id;
+ private String key;
+ private AccountType type;
+ private AccountStatus status;
+ private Long currentBalance;
+ private AccountGroup accountGroup;
+
+ 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;
+ }
+
+ public AccountGroup getAccountGroup() {
+ return accountGroup;
+ }
+
+ public void setAccountGroup(AccountGroup accountGroup) {
+ this.accountGroup = accountGroup;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/AccountGroup.java b/financer-web-client/src/main/java/de/financer/model/AccountGroup.java
new file mode 100644
index 0000000..8e0a90f
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/AccountGroup.java
@@ -0,0 +1,18 @@
+package de.financer.model;
+
+public class AccountGroup {
+ private Long id;
+ private String name;
+
+ public Long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/AccountStatus.java b/financer-web-client/src/main/java/de/financer/model/AccountStatus.java
new file mode 100644
index 0000000..9298e31
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/AccountStatus.java
@@ -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));
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/AccountType.java b/financer-web-client/src/main/java/de/financer/model/AccountType.java
new file mode 100644
index 0000000..763d398
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/AccountType.java
@@ -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 valueList() {
+ return Arrays.stream(AccountType.values()).map(AccountType::name).collect(Collectors.toList());
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/HolidayWeekendType.java b/financer-web-client/src/main/java/de/financer/model/HolidayWeekendType.java
new file mode 100644
index 0000000..6b79ab6
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/HolidayWeekendType.java
@@ -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,
+
+ /**
+ *
+ * Indicates that the action should be deferred to the next workday.
+ *
+ *
+ * Example 1:
+ * MO TU WE TH FR SA SO
+ * H WE WE -> Holiday/WeekEnd
+ * X -> Due date of action
+ * X' -> Deferred, effective due date of action
+ *
+ *
+ * Example 2:
+ * TU WE TH FR SA SO MO
+ * H WE WE -> Holiday/WeekEnd
+ * X -> Due date of action
+ * X' -> Deferred, effective due date of action
+ *
+ *
+ */
+ NEXT_WORKDAY,
+
+ /**
+ *
+ * Indicates that the action should preponed to the previous day
+ *
+ *
+ * Example 1:
+ * MO TU WE TH FR SA SO
+ * H WE WE -> Holiday/WeekEnd
+ * X -> Due date of action
+ * X' -> Earlier, effective due date of action
+ *
+ *
+ * Example 2:
+ * MO TU WE TH FR SA SO
+ * H WE WE -> Holiday/WeekEnd
+ * X -> Due date of action
+ * X' -> Earlier, effective due date of action
+ *
+ */
+ 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 valueList() {
+ return Arrays.stream(HolidayWeekendType.values()).map(HolidayWeekendType::name).collect(Collectors.toList());
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/IntervalType.java b/financer-web-client/src/main/java/de/financer/model/IntervalType.java
new file mode 100644
index 0000000..180d9ef
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/IntervalType.java
@@ -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 valueList() {
+ return Arrays.stream(IntervalType.values()).map(IntervalType::name).collect(Collectors.toList());
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/RecurringTransaction.java b/financer-web-client/src/main/java/de/financer/model/RecurringTransaction.java
new file mode 100644
index 0000000..a2d486a
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/RecurringTransaction.java
@@ -0,0 +1,92 @@
+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;
+ private boolean remind;
+
+ 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;
+ }
+
+ public boolean isRemind() {
+ return remind;
+ }
+
+ public void setRemind(boolean remind) {
+ this.remind = remind;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/model/Transaction.java b/financer-web-client/src/main/java/de/financer/model/Transaction.java
new file mode 100644
index 0000000..16c848d
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/model/Transaction.java
@@ -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;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java b/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java
new file mode 100644
index 0000000..d167e6c
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/util/ControllerUtils.java
@@ -0,0 +1,63 @@
+package de.financer.util;
+
+import de.financer.config.FinancerConfig;
+import de.financer.controller.Function;
+import de.financer.model.Account;
+import de.financer.model.AccountGroup;
+import de.financer.model.AccountStatus;
+import de.financer.model.RecurringTransaction;
+import de.financer.util.comparator.AccountByTypeByIdComparator;
+import org.apache.commons.collections4.IterableUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.ui.Model;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
+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 filterAndSortAccounts(Iterable accounts) {
+ return filterAndSortAccounts(accounts, false);
+ }
+
+ public static List filterAndSortAccounts(Iterable accounts, boolean showClosed) {
+ return IterableUtils.toList(accounts).stream()
+ .filter((acc) -> AccountStatus.OPEN
+ .equals(acc.getStatus()) || showClosed)
+ .sorted(new AccountByTypeByIdComparator())
+ .collect(Collectors.toList());
+ }
+
+ public static List sortAccountGroups(Iterable accountGroups) {
+ return IterableUtils.toList(accountGroups).stream()
+ .sorted(Comparator.comparing(AccountGroup::getName))
+ .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 filterEmptyEntries(Iterable recurringTransactions) {
+ return IterableUtils.toList(recurringTransactions).stream()
+ .filter((rt) -> rt.getFromAccount() != null && rt.getToAccount() != null)
+ .collect(Collectors.toList());
+ }
+
+ public static void addVersionAttribute(Model model, FinancerConfig financerConfig) {
+ model.addAttribute("financerVersion", financerConfig.getVersion());
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java b/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java
new file mode 100644
index 0000000..444d3d0
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/util/TransactionUtils.java
@@ -0,0 +1,64 @@
+package de.financer.util;
+
+import de.financer.model.Account;
+import de.financer.model.AccountType;
+import de.financer.model.Transaction;
+
+import static de.financer.model.AccountType.*;
+
+public class TransactionUtils {
+ public static final void adjustAmount(Transaction t, Account account) {
+ boolean isFrom = t.getFromAccount().getKey().equals(account.getKey());
+
+ if (AccountType.START.equals(t.getFromAccount().getType()) && AccountType.LIABILITY.equals(t.getToAccount().getType())) {
+ return;
+ }
+
+ if (isFrom) {
+ t.setAmount(t.getAmount() * TransactionUtils.getMultiplierFromAccount(account));
+ }
+ else {
+ t.setAmount(t.getAmount() * TransactionUtils.getMultiplierToAccount(account));
+ }
+ }
+
+ public static long getMultiplierFromAccount(Account fromAccount) {
+ // There is no multiplier if the from account is an EXPENSE account because
+ // it's not a valid from account type
+
+ final AccountType accountType = fromAccount.getType();
+
+ if (INCOME.equals(accountType)) {
+ return 1L;
+ } else if (BANK.equals(accountType)) {
+ return -1L;
+ } else if (CASH.equals(accountType)) {
+ return -1L;
+ } else if (LIABILITY.equals(accountType)) {
+ return 1L;
+ } else if (START.equals(accountType)) {
+ return 1L;
+ }
+
+ return 1L;
+ }
+
+ public static long getMultiplierToAccount(Account toAccount) {
+ // There are no multipliers for INCOME and START accounts
+ // because they are not valid to account types
+
+ final AccountType accountType = toAccount.getType();
+
+ if (BANK.equals(accountType)) {
+ return 1L;
+ } else if (CASH.equals(accountType)) {
+ return 1L;
+ } else if (LIABILITY.equals(accountType)) {
+ return -1L;
+ } else if (EXPENSE.equals(accountType)) {
+ return 1L;
+ }
+
+ return -1L;
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/util/comparator/AccountByTypeByIdComparator.java b/financer-web-client/src/main/java/de/financer/util/comparator/AccountByTypeByIdComparator.java
new file mode 100644
index 0000000..cf63757
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/util/comparator/AccountByTypeByIdComparator.java
@@ -0,0 +1,25 @@
+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 AccountByTypeByIdComparator implements Comparator {
+ //@formatter:off
+ private static final Map 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) {
+ int typeOrder = SORT_ORDER.get(o1.getType()).compareTo(SORT_ORDER.get(o2.getType()));
+
+ return typeOrder != 0 ? typeOrder : o1.getId().compareTo(o2.getId());
+ }
+}
diff --git a/financer-web-client/src/main/java/de/financer/util/comparator/TransactionByDateByIdDescComparator.java b/financer-web-client/src/main/java/de/financer/util/comparator/TransactionByDateByIdDescComparator.java
new file mode 100644
index 0000000..3ae2a9d
--- /dev/null
+++ b/financer-web-client/src/main/java/de/financer/util/comparator/TransactionByDateByIdDescComparator.java
@@ -0,0 +1,14 @@
+package de.financer.util.comparator;
+
+import de.financer.model.Transaction;
+
+import java.util.Comparator;
+
+public class TransactionByDateByIdDescComparator implements Comparator {
+ @Override
+ public int compare(Transaction o1, Transaction o2) {
+ int dateOrder = o1.getDate().compareTo(o2.getDate()) * -1;
+
+ return dateOrder != 0 ? dateOrder : o1.getId().compareTo(o2.getId()) * -1;
+ }
+}
diff --git a/financer-web-client/src/main/resources/config/application-dev.properties b/financer-web-client/src/main/resources/config/application-dev.properties
new file mode 100644
index 0000000..7bbdbf2
--- /dev/null
+++ b/financer-web-client/src/main/resources/config/application-dev.properties
@@ -0,0 +1,4 @@
+# Hibernate
+spring.jpa.show-sql=true
+
+financer.serverUrl=http://localhost:8089/financer-server/
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/config/application.properties b/financer-web-client/src/main/resources/config/application.properties
new file mode 100644
index 0000000..6db2d9f
--- /dev/null
+++ b/financer-web-client/src/main/resources/config/application.properties
@@ -0,0 +1,30 @@
+###
+### 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@
+
+# This properties are just for the embedded Tomcat used while developing
+# In a standalone servlet container instance the context path is usually defined by the name
+# of the .war file. See property 'finalName' in the POM for more info.
+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
+logging.file.max-history=7
+logging.file.max-size=50MB
+
+# The date format of the client-supplied date string, used to parse the string into a proper object
+financer.dateFormat=dd.MM.yyyy
+financer.version=@project.version@
+financer.serverUrl=http://localhost:8080/financer-server/
+
+spring.messages.basename=i18n/message
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/i18n/message.properties b/financer-web-client/src/main/resources/i18n/message.properties
new file mode 100644
index 0000000..9d7b159
--- /dev/null
+++ b/financer-web-client/src/main/resources/i18n/message.properties
@@ -0,0 +1,159 @@
+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.available-actions.create-account-group=Create new account group
+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=Account
+financer.account-overview.table-header.group=Group
+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.label.group=Group\:
+financer.account-new.submit=Create account
+
+financer.account-group-new.title=financer\: create new account group
+financer.account-group-new.label.name=Name\:
+financer.account-group-new.submit=Create account group
+
+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.label.remind=Remind if due\:
+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.available-actions.back-to-overview=Back to overview
+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.details.group=Group\:
+financer.account-details.table-header.actions=Actions
+financer.account-details.table.actions.deleteTransaction=Delete
+
+financer.recurring-to-transaction-with-amount.title=financer\: create transaction from recurring with amount
+financer.recurring-to-transaction-with-amount.label.amount=Amount\:
+financer.recurring-to-transaction-with-amount.submit=Create
+
+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-group-new=financer\: create new account group
+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.heading.recurring-to-transaction-with-amount=financer\: create transaction from recurring with amount
+
+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.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!
+financer.error-message.DUPLICATE_ACCOUNT_KEY=An account with the given key already exists!
+financer.error-message.DUPLICATE_ACCOUNT_GROUP_NAME=An account group with the given key already exists!
+financer.error-message.ACCOUNT_GROUP_NOT_FOUND=The account group could not be found!
\ No newline at end of file
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
new file mode 100644
index 0000000..ddb1924
--- /dev/null
+++ b/financer-web-client/src/main/resources/i18n/message_de_DE.properties
@@ -0,0 +1,131 @@
+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.available-actions.create-account-group=Neue Konto-Gruppe erstellen
+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=Konto
+financer.account-overview.table-header.group=Gruppe
+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.label.group=Gruppe\:
+financer.account-new.submit=Konto erstellen
+
+financer.account-group-new.title=financer\: Neue Konto-Gruppe erstellen
+financer.account-group-new.label.name=Name\:
+financer.account-group-new.submit=Konto-Gruppe 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.label.remind=Erinnern wenn f\u00E4llig\:
+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\u00F6schen
+
+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.available-actions.back-to-overview=Zur\u00FCck zur \u00DCbersicht
+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.details.group=Gruppe\:
+financer.account-details.table-header.actions=Aktionen
+financer.account-details.table.actions.deleteTransaction=L\u00F6schen
+
+financer.recurring-to-transaction-with-amount.title=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
+financer.recurring-to-transaction-with-amount.label.amount=Betrag\:
+financer.recurring-to-transaction-with-amount.submit=Erstellen
+
+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-group-new=financer\: Neue Konto-Gruppe 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
+financer.heading.recurring-to-transaction-with-amount=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/static/css/main.css b/financer-web-client/src/main/resources/static/css/main.css
new file mode 100644
index 0000000..8f3eef5
--- /dev/null
+++ b/financer-web-client/src/main/resources/static/css/main.css
@@ -0,0 +1,93 @@
+#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 * {
+ display: block !important;
+ margin-bottom: 0.1em;
+ }
+}
+
+#action-container * {
+ display: inline-grid;
+ padding-inline-end: 1em;
+ margin-bottom: 0.2em;
+}
+
+#account-details-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 *,
+#recurring-to-transaction-with-amount-form *,
+#new-account-group-form * {
+ display: block;
+ margin-top: 1em;
+ width: 20em;
+ box-sizing: border-box;
+}
+
+#footer-container * {
+ display: block;
+}
+
+#footer-container > hr {
+ width: 100%;
+}
+
+#footer-container > span {
+ text-align: center;
+ font-size: 0.9em;
+}
+
+.errorMessage {
+ color: #ff6666;
+ display: block;
+}
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/account/accountDetails.html b/financer-web-client/src/main/resources/templates/account/accountDetails.html
new file mode 100644
index 0000000..e393186
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/account/accountDetails.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/account/accountOverview.html b/financer-web-client/src/main/resources/templates/account/accountOverview.html
new file mode 100644
index 0000000..3abcf48
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/account/accountOverview.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/account/newAccount.html b/financer-web-client/src/main/resources/templates/account/newAccount.html
new file mode 100644
index 0000000..7a10e30
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/account/newAccount.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html b/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html
new file mode 100644
index 0000000..d1644b5
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/includes/footer.html b/financer-web-client/src/main/resources/templates/includes/footer.html
new file mode 100644
index 0000000..8c9bd8b
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/includes/footer.html
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html b/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html
new file mode 100644
index 0000000..3d1c603
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html
new file mode 100644
index 0000000..f96467f
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html
new file mode 100644
index 0000000..8a72fba
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/main/resources/templates/transaction/newTransaction.html b/financer-web-client/src/main/resources/templates/transaction/newTransaction.html
new file mode 100644
index 0000000..791f08f
--- /dev/null
+++ b/financer-web-client/src/main/resources/templates/transaction/newTransaction.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/financer-web-client/src/test/java/de/financer/FinancerApplicationBootTest.java b/financer-web-client/src/test/java/de/financer/FinancerApplicationBootTest.java
new file mode 100644
index 0000000..21ec49f
--- /dev/null
+++ b/financer-web-client/src/test/java/de/financer/FinancerApplicationBootTest.java
@@ -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);
+ }
+}
diff --git a/financer-web-client/src/test/resources/application-integrationtest.properties b/financer-web-client/src/test/resources/application-integrationtest.properties
new file mode 100644
index 0000000..1fcad4a
--- /dev/null
+++ b/financer-web-client/src/test/resources/application-integrationtest.properties
@@ -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