From 67bd7f951ae4bb26b979ca7ebe3071e857686307 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 24 Mar 2019 23:28:15 +0100 Subject: [PATCH] WIP Initial commit for financer web client --- .gitignore | 2 + pom.xml | 141 +++++++++++++++++ .../java/de/financer/FinancerApplication.java | 20 +++ src/main/java/de/financer/ResponseReason.java | 53 +++++++ .../de/financer/config/FinancerConfig.java | 31 ++++ .../controller/AccountController.java | 137 +++++++++++++++++ .../java/de/financer/controller/Function.java | 32 ++++ .../RecurringTransactionController.java | 113 ++++++++++++++ .../controller/TransactionController.java | 59 ++++++++ .../NoExceptionResponseErrorHandler.java | 18 +++ .../template/FinancerRestTemplate.java | 42 ++++++ .../template/GetAccountByKeyTemplate.java | 21 +++ .../template/GetAllAccountsTemplate.java | 16 ++ ...llActiveRecurringTransactionsTemplate.java | 16 ++ ...RecurringTransactionsDueTodayTemplate.java | 16 ++ .../GetAllRecurringTransactionsTemplate.java | 16 ++ .../GetAllTransactionsForAccountTemplate.java | 21 +++ .../controller/template/StringTemplate.java | 13 ++ .../java/de/financer/form/NewAccountForm.java | 22 +++ .../form/NewRecurringTransactionForm.java | 76 ++++++++++ .../de/financer/form/NewTransactionForm.java | 49 ++++++ src/main/java/de/financer/model/Account.java | 45 ++++++ .../java/de/financer/model/AccountStatus.java | 20 +++ .../java/de/financer/model/AccountType.java | 38 +++++ .../de/financer/model/HolidayWeekendType.java | 71 +++++++++ .../java/de/financer/model/IntervalType.java | 36 +++++ .../financer/model/RecurringTransaction.java | 83 ++++++++++ .../java/de/financer/model/Transaction.java | 65 ++++++++ .../de/financer/util/ControllerUtils.java | 50 ++++++ .../comparator/AccountByTypeComparator.java | 23 +++ .../config/application-dev.properties | 2 + .../resources/config/application.properties | 25 +++ src/main/resources/i18n/message.properties | 142 ++++++++++++++++++ .../resources/i18n/message_de_DE.properties | 116 ++++++++++++++ src/main/resources/static/css/main.css | 67 +++++++++ .../templates/account/accountDetails.html | 59 ++++++++ .../templates/account/accountOverview.html | 54 +++++++ .../templates/account/newAccount.html | 22 +++ .../newRecurringTransaction.html | 45 ++++++ .../recurringTransactionList.html | 53 +++++++ .../templates/transaction/newTransaction.html | 33 ++++ .../financer/FinancerApplicationBootTest.java | 31 ++++ .../application-integrationtest.properties | 5 + 43 files changed, 1999 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/de/financer/FinancerApplication.java create mode 100644 src/main/java/de/financer/ResponseReason.java create mode 100644 src/main/java/de/financer/config/FinancerConfig.java create mode 100644 src/main/java/de/financer/controller/AccountController.java create mode 100644 src/main/java/de/financer/controller/Function.java create mode 100644 src/main/java/de/financer/controller/RecurringTransactionController.java create mode 100644 src/main/java/de/financer/controller/TransactionController.java create mode 100644 src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java create mode 100644 src/main/java/de/financer/controller/template/FinancerRestTemplate.java create mode 100644 src/main/java/de/financer/controller/template/GetAccountByKeyTemplate.java create mode 100644 src/main/java/de/financer/controller/template/GetAllAccountsTemplate.java create mode 100644 src/main/java/de/financer/controller/template/GetAllActiveRecurringTransactionsTemplate.java create mode 100644 src/main/java/de/financer/controller/template/GetAllRecurringTransactionsDueTodayTemplate.java create mode 100644 src/main/java/de/financer/controller/template/GetAllRecurringTransactionsTemplate.java create mode 100644 src/main/java/de/financer/controller/template/GetAllTransactionsForAccountTemplate.java create mode 100644 src/main/java/de/financer/controller/template/StringTemplate.java create mode 100644 src/main/java/de/financer/form/NewAccountForm.java create mode 100644 src/main/java/de/financer/form/NewRecurringTransactionForm.java create mode 100644 src/main/java/de/financer/form/NewTransactionForm.java create mode 100644 src/main/java/de/financer/model/Account.java create mode 100644 src/main/java/de/financer/model/AccountStatus.java create mode 100644 src/main/java/de/financer/model/AccountType.java create mode 100644 src/main/java/de/financer/model/HolidayWeekendType.java create mode 100644 src/main/java/de/financer/model/IntervalType.java create mode 100644 src/main/java/de/financer/model/RecurringTransaction.java create mode 100644 src/main/java/de/financer/model/Transaction.java create mode 100644 src/main/java/de/financer/util/ControllerUtils.java create mode 100644 src/main/java/de/financer/util/comparator/AccountByTypeComparator.java create mode 100644 src/main/resources/config/application-dev.properties create mode 100644 src/main/resources/config/application.properties create mode 100644 src/main/resources/i18n/message.properties create mode 100644 src/main/resources/i18n/message_de_DE.properties create mode 100644 src/main/resources/static/css/main.css create mode 100644 src/main/resources/templates/account/accountDetails.html create mode 100644 src/main/resources/templates/account/accountOverview.html create mode 100644 src/main/resources/templates/account/newAccount.html create mode 100644 src/main/resources/templates/recurringTransaction/newRecurringTransaction.html create mode 100644 src/main/resources/templates/recurringTransaction/recurringTransactionList.html create mode 100644 src/main/resources/templates/transaction/newTransaction.html create mode 100644 src/test/java/de/financer/FinancerApplicationBootTest.java create mode 100644 src/test/resources/application-integrationtest.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3cec76 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +financer-web-client.log* +.attach* \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..aab892e --- /dev/null +++ b/pom.xml @@ -0,0 +1,141 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.1.2.RELEASE + + + + de.77zzcx7.financer + financer-web-client + 1-SNAPSHOT + ${packaging.type} + The web client part of the financer application - a simple app to manage your personal finances + financer-web-client + + + UTF-8 + 1.9 + 1.9 + 1.9 + jar + dev + + 000001 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-collections4 + 4.3 + + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.restdocs + spring-restdocs-mockmvc + test + + + junit + junit + 4.12 + test + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*IntegrationTest + + + + + + + + + integration-tests + + + + maven-surefire-plugin + + + integration-test + + test + + + + none + + + **/*IntegrationTest + + + + + + + + + + + build-war + + war + postgres + + + ${project.artifactId}##${parallelDeploymentVersion} + + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + + + diff --git a/src/main/java/de/financer/FinancerApplication.java b/src/main/java/de/financer/FinancerApplication.java new file mode 100644 index 0000000..8737a0b --- /dev/null +++ b/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/src/main/java/de/financer/ResponseReason.java b/src/main/java/de/financer/ResponseReason.java new file mode 100644 index 0000000..88532a1 --- /dev/null +++ b/src/main/java/de/financer/ResponseReason.java @@ -0,0 +1,53 @@ +package de.financer; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public enum ResponseReason { + OK(HttpStatus.OK), + UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR), + FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_DATE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR), + AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_AMOUNT(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_BOOKING_ACCOUNTS(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_FIRST_OCCURRENCE(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), + RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), + INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR), + TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR), + ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR); + + private HttpStatus httpStatus; + + ResponseReason(HttpStatus httpStatus) { + this.httpStatus = httpStatus; + } + + public ResponseEntity toResponseEntity() { + return new ResponseEntity<>(this.name(), this.httpStatus); + } + + public static ResponseReason fromResponseEntity(ResponseEntity entity) { + for (ResponseReason reason : values()) { + if (reason.name().equals(entity.getBody())) { + return reason; + } + } + + return UNKNOWN_ERROR; + } +} diff --git a/src/main/java/de/financer/config/FinancerConfig.java b/src/main/java/de/financer/config/FinancerConfig.java new file mode 100644 index 0000000..91a368d --- /dev/null +++ b/src/main/java/de/financer/config/FinancerConfig.java @@ -0,0 +1,31 @@ +package de.financer.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "financer") +public class FinancerConfig { + private static final Logger LOGGER = LoggerFactory.getLogger(FinancerConfig.class); + + private String serverUrl; + private String dateFormat; + + public String getServerUrl() { + return serverUrl; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + public String getDateFormat() { + return dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } +} diff --git a/src/main/java/de/financer/controller/AccountController.java b/src/main/java/de/financer/controller/AccountController.java new file mode 100644 index 0000000..9fa2b36 --- /dev/null +++ b/src/main/java/de/financer/controller/AccountController.java @@ -0,0 +1,137 @@ +package de.financer.controller; + +import de.financer.ResponseReason; +import de.financer.config.FinancerConfig; +import de.financer.controller.template.*; +import de.financer.form.NewAccountForm; +import de.financer.model.*; +import de.financer.util.ControllerUtils; +import org.apache.commons.collections4.IterableUtils; +import org.apache.commons.lang3.BooleanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.util.UriComponentsBuilder; + +@Controller +public class AccountController { + @Autowired + private FinancerConfig financerConfig; + + @GetMapping("/accountOverview") + public String accountOverview(String showClosed, Model model) { + final ResponseEntity> 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); + + return "account/accountOverview"; + } + + @GetMapping("/newAccount") + public String newAccount(Model model) { + model.addAttribute("accounttypes", AccountType.valueList()); + model.addAttribute("newAccountForm", new NewAccountForm()); + + return "account/newAccount"; + } + + @PostMapping("/saveAccount") + public String saveAccont(NewAccountForm newAccountForm, Model model) { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT)) + .queryParam("key", newAccountForm.getKey()) + .queryParam("type", newAccountForm.getType()); + + final ResponseEntity response = new StringTemplate().exchange(builder); + final ResponseReason responseReason = ResponseReason.fromResponseEntity(response); + + if (!ResponseReason.OK.equals(responseReason)) { + model.addAttribute("form", newAccountForm); + model.addAttribute("accounttypes", AccountType.valueList()); + model.addAttribute("errorMessage", responseReason.name()); + + return "account/newAccount"; + } + + return "redirect:/accountOverview"; + } + + @GetMapping("/accountDetails") + public String accountDetails(String key, Model model) { + final ResponseEntity response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key); + final Account account = response.getBody(); + final ResponseEntity> transactionResponse = new GetAllTransactionsForAccountTemplate() + .exchange(this.financerConfig, account.getKey()); + + model.addAttribute("account", account); + model.addAttribute("transactions", IterableUtils.toList(transactionResponse.getBody())); + model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); + + return "account/accountDetails"; + } + + @GetMapping("/closeAccount") + public String closeAccount(String key, Model model) { + final UriComponentsBuilder closeBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CLOSE_ACCOUNT)) + .queryParam("key", key); + + final ResponseEntity 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()); + + model.addAttribute("account", account); + model.addAttribute("transactions", IterableUtils.toList(transactionResponse.getBody())); + model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); + model.addAttribute("errorMessage", responseReason.name()); + + return "account/accountDetails"; + } + + return "redirect:/accountOverview"; + } + + @GetMapping("/openAccount") + public String openAccount(String key, Model model) { + final UriComponentsBuilder openBuilder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_OPEN_ACCOUNT)) + .queryParam("key", key); + + final ResponseEntity 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()); + + model.addAttribute("account", account); + model.addAttribute("transactions", IterableUtils.toList(transactionResponse.getBody())); + model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); + model.addAttribute("errorMessage", responseReason.name()); + + return "account/accountDetails"; + } + + return "redirect:/accountOverview"; + } +} diff --git a/src/main/java/de/financer/controller/Function.java b/src/main/java/de/financer/controller/Function.java new file mode 100644 index 0000000..e97bdcb --- /dev/null +++ b/src/main/java/de/financer/controller/Function.java @@ -0,0 +1,32 @@ +package de.financer.controller; + +public enum Function { + ACC_GET_BY_KEY("accounts/getByKey"), + ACC_GET_ALL("accounts/getAll"), + ACC_CREATE_ACCOUNT("accounts/createAccount"), + ACC_CLOSE_ACCOUNT("accounts/closeAccount"), + ACC_OPEN_ACCOUNT("accounts/openAccount"), + + TR_GET_ALL("transactions/getAll"), + TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"), + TR_CREATE_TRANSACTION("transactions/createTransaction"), + TR_DELETE_TRANSACTION("transactions/deleteTransaction"), + + RT_GET_ALL("recurringTransactions/getAll"), + RT_GET_ALL_ACTIVE("recurringTransactions/getAllActive"), + RT_GET_ALL_FOR_ACCOUNT("recurringTransactions/getAllForAccount"), + RT_GET_ALL_DUE_TODAY("recurringTransactions/getAllDueToday"), + RT_CREATE_RECURRING_TRANSACTION("recurringTransactions/createRecurringTransaction"), + RT_DELETE_RECURRING_TRANSACTION("recurringTransactions/deleteRecurringTransaction"), + RT_CREATE_TRANSACTION("recurringTransactions/createTransaction"); + + private String path; + + Function(String path) { + this.path = path; + } + + public String getPath() { + return this.path; + } +} diff --git a/src/main/java/de/financer/controller/RecurringTransactionController.java b/src/main/java/de/financer/controller/RecurringTransactionController.java new file mode 100644 index 0000000..397efc1 --- /dev/null +++ b/src/main/java/de/financer/controller/RecurringTransactionController.java @@ -0,0 +1,113 @@ +package de.financer.controller; + +import de.financer.ResponseReason; +import de.financer.config.FinancerConfig; +import de.financer.controller.template.*; +import de.financer.form.NewRecurringTransactionForm; +import de.financer.model.Account; +import de.financer.model.HolidayWeekendType; +import de.financer.model.IntervalType; +import de.financer.model.RecurringTransaction; +import de.financer.util.ControllerUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.util.UriComponentsBuilder; + +@Controller +public class RecurringTransactionController { + + @Autowired + private FinancerConfig financerConfig; + + @GetMapping("/newRecurringTransaction") + public String newRecurringTransaction(Model model) { + final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig); + + model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody())); + model.addAttribute("intervalTypes", IntervalType.valueList()); + model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList()); + model.addAttribute("newRecurringTransactionForm", new NewRecurringTransactionForm()); + + return "recurringTransaction/newRecurringTransaction"; + } + + @PostMapping("/saveRecurringTransaction") + public String saveRecurringTransaction(NewRecurringTransactionForm newRecurringTransactionForm, Model model) { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.RT_CREATE_RECURRING_TRANSACTION)) + .queryParam("fromAccountKey", newRecurringTransactionForm + .getFromAccountKey()) + .queryParam("toAccountKey", newRecurringTransactionForm + .getToAccountKey()) + .queryParam("amount", newRecurringTransactionForm + .getAmount()) + .queryParam("firstOccurrence", ControllerUtils + .formatDate(this.financerConfig, newRecurringTransactionForm + .getFirstOccurrence())) + .queryParam("lastOccurrence", ControllerUtils + .formatDate(this.financerConfig, newRecurringTransactionForm + .getLastOccurrence())) + .queryParam("holidayWeekendType", newRecurringTransactionForm + .getHolidayWeekendType()) + .queryParam("intervalType", newRecurringTransactionForm + .getIntervalType()) + .queryParam("description", newRecurringTransactionForm + .getDescription()); + + final ResponseEntity 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", newRecurringTransactionForm); + model.addAttribute("errorMessage", responseReason.name()); + + 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"); + + 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"); + + 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"); + + return "recurringTransaction/recurringTransactionList"; + } +} diff --git a/src/main/java/de/financer/controller/TransactionController.java b/src/main/java/de/financer/controller/TransactionController.java new file mode 100644 index 0000000..6eeec4c --- /dev/null +++ b/src/main/java/de/financer/controller/TransactionController.java @@ -0,0 +1,59 @@ +package de.financer.controller; + +import de.financer.ResponseReason; +import de.financer.config.FinancerConfig; +import de.financer.controller.template.GetAllAccountsTemplate; +import de.financer.controller.template.StringTemplate; +import de.financer.form.NewTransactionForm; +import de.financer.model.Account; +import de.financer.util.ControllerUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.util.UriComponentsBuilder; + +@Controller +public class TransactionController { + + @Autowired + private FinancerConfig financerConfig; + + @GetMapping("/newTransaction") + public String newTransaction(Model model) { + final ResponseEntity> response = new GetAllAccountsTemplate().exchange(this.financerConfig); + + model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody())); + model.addAttribute("newTransactionForm", new NewTransactionForm()); + + return "transaction/newTransaction"; + } + + @PostMapping("/saveTransaction") + public String saveTransaction(NewTransactionForm newTransactionForm, Model model) { + final UriComponentsBuilder builder = UriComponentsBuilder + .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_CREATE_TRANSACTION)) + .queryParam("fromAccountKey", newTransactionForm.getFromAccountKey()) + .queryParam("toAccountKey", newTransactionForm.getToAccountKey()) + .queryParam("amount", newTransactionForm.getAmount()) + .queryParam("date", ControllerUtils.formatDate(this.financerConfig, newTransactionForm.getDate())) + .queryParam("description", newTransactionForm.getDescription()); + + final ResponseEntity 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", newTransactionForm); + model.addAttribute("errorMessage", responseReason.name()); + + return "transaction/newTransaction"; + } + + return "redirect:/accountOverview"; + } +} diff --git a/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java b/src/main/java/de/financer/controller/handler/NoExceptionResponseErrorHandler.java new file mode 100644 index 0000000..16c6eaf --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/FinancerRestTemplate.java b/src/main/java/de/financer/controller/template/FinancerRestTemplate.java new file mode 100644 index 0000000..bd4d604 --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/GetAccountByKeyTemplate.java b/src/main/java/de/financer/controller/template/GetAccountByKeyTemplate.java new file mode 100644 index 0000000..d2552b4 --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/GetAllAccountsTemplate.java b/src/main/java/de/financer/controller/template/GetAllAccountsTemplate.java new file mode 100644 index 0000000..b1933b5 --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/GetAllActiveRecurringTransactionsTemplate.java b/src/main/java/de/financer/controller/template/GetAllActiveRecurringTransactionsTemplate.java new file mode 100644 index 0000000..8d779b6 --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsDueTodayTemplate.java b/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsDueTodayTemplate.java new file mode 100644 index 0000000..a63fa03 --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsTemplate.java b/src/main/java/de/financer/controller/template/GetAllRecurringTransactionsTemplate.java new file mode 100644 index 0000000..071bb51 --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/GetAllTransactionsForAccountTemplate.java b/src/main/java/de/financer/controller/template/GetAllTransactionsForAccountTemplate.java new file mode 100644 index 0000000..8f3b4de --- /dev/null +++ b/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/src/main/java/de/financer/controller/template/StringTemplate.java b/src/main/java/de/financer/controller/template/StringTemplate.java new file mode 100644 index 0000000..96148e0 --- /dev/null +++ b/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/src/main/java/de/financer/form/NewAccountForm.java b/src/main/java/de/financer/form/NewAccountForm.java new file mode 100644 index 0000000..fd3218d --- /dev/null +++ b/src/main/java/de/financer/form/NewAccountForm.java @@ -0,0 +1,22 @@ +package de.financer.form; + +public class NewAccountForm { + private String key; + private String type; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/main/java/de/financer/form/NewRecurringTransactionForm.java b/src/main/java/de/financer/form/NewRecurringTransactionForm.java new file mode 100644 index 0000000..3f7d419 --- /dev/null +++ b/src/main/java/de/financer/form/NewRecurringTransactionForm.java @@ -0,0 +1,76 @@ +package de.financer.form; + +public class NewRecurringTransactionForm { + private String fromAccountKey; + private String toAccountKey; + private String amount; + private String description; + private String firstOccurrence; + private String lastOccurrence; + private String intervalType; + private String holidayWeekendType; + + public String getFromAccountKey() { + return fromAccountKey; + } + + public void setFromAccountKey(String fromAccountKey) { + this.fromAccountKey = fromAccountKey; + } + + public String getToAccountKey() { + return toAccountKey; + } + + public void setToAccountKey(String toAccountKey) { + this.toAccountKey = toAccountKey; + } + + public String getAmount() { + return amount; + } + + public void setAmount(String amount) { + this.amount = amount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getFirstOccurrence() { + return firstOccurrence; + } + + public void setFirstOccurrence(String firstOccurrence) { + this.firstOccurrence = firstOccurrence; + } + + public String getLastOccurrence() { + return lastOccurrence; + } + + public void setLastOccurrence(String lastOccurrence) { + this.lastOccurrence = lastOccurrence; + } + + public String getIntervalType() { + return intervalType; + } + + public void setIntervalType(String intervalType) { + this.intervalType = intervalType; + } + + public String getHolidayWeekendType() { + return holidayWeekendType; + } + + public void setHolidayWeekendType(String holidayWeekendType) { + this.holidayWeekendType = holidayWeekendType; + } +} diff --git a/src/main/java/de/financer/form/NewTransactionForm.java b/src/main/java/de/financer/form/NewTransactionForm.java new file mode 100644 index 0000000..89dcd6b --- /dev/null +++ b/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/src/main/java/de/financer/model/Account.java b/src/main/java/de/financer/model/Account.java new file mode 100644 index 0000000..0e4101e --- /dev/null +++ b/src/main/java/de/financer/model/Account.java @@ -0,0 +1,45 @@ +package de.financer.model; + +public class Account { + private Long id; + private String key; + private AccountType type; + private AccountStatus status; + private Long currentBalance; + + public Long getId() { + return id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public AccountType getType() { + return type; + } + + public void setType(AccountType type) { + this.type = type; + } + + public AccountStatus getStatus() { + return status; + } + + public void setStatus(AccountStatus status) { + this.status = status; + } + + public Long getCurrentBalance() { + return currentBalance; + } + + public void setCurrentBalance(Long currentBalance) { + this.currentBalance = currentBalance; + } +} diff --git a/src/main/java/de/financer/model/AccountStatus.java b/src/main/java/de/financer/model/AccountStatus.java new file mode 100644 index 0000000..9298e31 --- /dev/null +++ b/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/src/main/java/de/financer/model/AccountType.java b/src/main/java/de/financer/model/AccountType.java new file mode 100644 index 0000000..763d398 --- /dev/null +++ b/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/src/main/java/de/financer/model/HolidayWeekendType.java b/src/main/java/de/financer/model/HolidayWeekendType.java new file mode 100644 index 0000000..6b79ab6 --- /dev/null +++ b/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/src/main/java/de/financer/model/IntervalType.java b/src/main/java/de/financer/model/IntervalType.java new file mode 100644 index 0000000..180d9ef --- /dev/null +++ b/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/src/main/java/de/financer/model/RecurringTransaction.java b/src/main/java/de/financer/model/RecurringTransaction.java new file mode 100644 index 0000000..417596f --- /dev/null +++ b/src/main/java/de/financer/model/RecurringTransaction.java @@ -0,0 +1,83 @@ +package de.financer.model; + +import java.time.LocalDate; + +public class RecurringTransaction { + private Long id; + private Account fromAccount; + private Account toAccount; + private String description; + private Long amount; + private IntervalType intervalType; + private LocalDate firstOccurrence; + private LocalDate lastOccurrence; + private HolidayWeekendType holidayWeekendType; + + public Long getId() { + return id; + } + + public Account getFromAccount() { + return fromAccount; + } + + public void setFromAccount(Account fromAccount) { + this.fromAccount = fromAccount; + } + + public Account getToAccount() { + return toAccount; + } + + public void setToAccount(Account toAccount) { + this.toAccount = toAccount; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Long getAmount() { + return amount; + } + + public void setAmount(Long amount) { + this.amount = amount; + } + + public HolidayWeekendType getHolidayWeekendType() { + return holidayWeekendType; + } + + public void setHolidayWeekendType(HolidayWeekendType holidayWeekendType) { + this.holidayWeekendType = holidayWeekendType; + } + + public LocalDate getLastOccurrence() { + return lastOccurrence; + } + + public void setLastOccurrence(LocalDate lastOccurrence) { + this.lastOccurrence = lastOccurrence; + } + + public LocalDate getFirstOccurrence() { + return firstOccurrence; + } + + public void setFirstOccurrence(LocalDate firstOccurrence) { + this.firstOccurrence = firstOccurrence; + } + + public IntervalType getIntervalType() { + return intervalType; + } + + public void setIntervalType(IntervalType intervalType) { + this.intervalType = intervalType; + } +} diff --git a/src/main/java/de/financer/model/Transaction.java b/src/main/java/de/financer/model/Transaction.java new file mode 100644 index 0000000..16c848d --- /dev/null +++ b/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/src/main/java/de/financer/util/ControllerUtils.java b/src/main/java/de/financer/util/ControllerUtils.java new file mode 100644 index 0000000..b9debf2 --- /dev/null +++ b/src/main/java/de/financer/util/ControllerUtils.java @@ -0,0 +1,50 @@ +package de.financer.util; + +import de.financer.config.FinancerConfig; +import de.financer.controller.Function; +import de.financer.model.Account; +import de.financer.model.AccountStatus; +import de.financer.model.RecurringTransaction; +import de.financer.util.comparator.AccountByTypeComparator; +import org.apache.commons.collections4.IterableUtils; +import org.apache.commons.lang3.StringUtils; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +public class ControllerUtils { + public static String buildUrl(FinancerConfig financerConfig, Function function) { + return String.format("%s%s", financerConfig.getServerUrl(), function.getPath()); + } + + public static List 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 AccountByTypeComparator()) + .collect(Collectors.toList()); + } + + public static String formatDate(FinancerConfig financerConfig, String dateFromForm) { + if (StringUtils.isEmpty(dateFromForm)) { + return null; + } + + // The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date + final LocalDate formatted = LocalDate.parse(dateFromForm, DateTimeFormatter.ofPattern("yyyy-MM-dd")); + + return formatted.format(DateTimeFormatter.ofPattern(financerConfig.getDateFormat())); + } + + public static List filterEmptyEntries(Iterable recurringTransactions) { + return IterableUtils.toList(recurringTransactions).stream() + .filter((rt) -> rt.getFromAccount() != null && rt.getToAccount() != null) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/de/financer/util/comparator/AccountByTypeComparator.java b/src/main/java/de/financer/util/comparator/AccountByTypeComparator.java new file mode 100644 index 0000000..4723d9c --- /dev/null +++ b/src/main/java/de/financer/util/comparator/AccountByTypeComparator.java @@ -0,0 +1,23 @@ +package de.financer.util.comparator; + +import de.financer.model.Account; +import de.financer.model.AccountType; + +import java.util.Comparator; +import java.util.Map; + +public class AccountByTypeComparator implements Comparator { + //@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) { + return SORT_ORDER.get(o1.getType()).compareTo(SORT_ORDER.get(o2.getType())); + } +} diff --git a/src/main/resources/config/application-dev.properties b/src/main/resources/config/application-dev.properties new file mode 100644 index 0000000..000f956 --- /dev/null +++ b/src/main/resources/config/application-dev.properties @@ -0,0 +1,2 @@ +# Hibernate +spring.jpa.show-sql=true \ No newline at end of file diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties new file mode 100644 index 0000000..3da6796 --- /dev/null +++ b/src/main/resources/config/application.properties @@ -0,0 +1,25 @@ +### +### This is the main configuration file of the application. +### Filtering of the @...@ values happens via the maven-resource-plugin. The execution of the plugin is configured in +### the Spring Boot parent POM. + +spring.profiles.active=@activeProfiles@ + +server.servlet.context-path=/financer-web-client +server.port=8090 + +info.app.name=Financer Web Client +info.app.description=A web interface for the financer server application +info.build.group=@project.groupId@ +info.build.artifact=@project.artifactId@ +info.build.version=@project.version@ + +logging.level.de.financer=DEBUG +logging.file=financer-web-client.log + +# The date format of the client-supplied date string, used to parse the string into a proper object +financer.dateFormat=dd.MM.yyyy + +financer.serverUrl=http://localhost:8089/financer-server/ + +spring.messages.basename=i18n/message \ No newline at end of file diff --git a/src/main/resources/i18n/message.properties b/src/main/resources/i18n/message.properties new file mode 100644 index 0000000..b2adb1f --- /dev/null +++ b/src/main/resources/i18n/message.properties @@ -0,0 +1,142 @@ +financer.account-overview.title=financer\: overview +financer.account-overview.available-actions=Available actions\: +financer.account-overview.available-actions.show-closed=Show closed accounts +financer.account-overview.available-actions.hide-closed=Hide closed accounts +financer.account-overview.available-actions.create-account=Create new account +financer.account-overview.available-actions.create-transaction=Create new transaction +financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction +financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions +financer.account-overview.status=Status\: +financer.account-overview.status.recurring-transaction-due-today=Recurring transactions due today\: +financer.account-overview.status.recurring-transaction-active=Active recurring transactions\: +financer.account-overview.table-header.id=ID +financer.account-overview.table-header.key=Key +financer.account-overview.table-header.balance=Current Balance +financer.account-overview.table-header.type=Type +financer.account-overview.table-header.status=Status + +financer.account-new.title=financer\: create new account +financer.account-new.label.key=Key\: +financer.account-new.label.type=Type\: +financer.account-new.submit=Create account + +financer.transaction-new.title=financer\: create new transaction +financer.transaction-new.label.from-account=From account\: +financer.transaction-new.label.to-account=To account\: +financer.transaction-new.label.amount=Amount\: +financer.transaction-new.label.date=Date\: +financer.transaction-new.label.description=Description\: +financer.transaction-new.submit=Create transaction +financer.transaction-new.account-type.BANK={0}|Bank|{1} +financer.transaction-new.account-type.CASH={0}|Cash|{1} +financer.transaction-new.account-type.INCOME={0}|Income|{1} +financer.transaction-new.account-type.LIABILITY={0}|Liability|{1} +financer.transaction-new.account-type.EXPENSE={0}|Expense|{1} +financer.transaction-new.account-type.START={0}|Start|{1} + +financer.recurring-transaction-new.title=financer\: create new recurring transaction +financer.recurring-transaction-new.label.from-account=From account\: +financer.recurring-transaction-new.label.to-account=To account\: +financer.recurring-transaction-new.label.amount=Amount\: +financer.recurring-transaction-new.label.first-occurrence=First occurrence\: +financer.recurring-transaction-new.label.last-occurrence=Last occurrence\: +financer.recurring-transaction-new.label.interval-type=Interval\: +financer.recurring-transaction-new.label.holiday-weekend-type=Holiday/weekend rule\: +financer.recurring-transaction-new.label.description=Description\: +financer.recurring-transaction-new.submit=Create recurring transaction +financer.recurring-transaction-new.account-type.BANK={0}|Bank|{1} +financer.recurring-transaction-new.account-type.CASH={0}|Cash|{1} +financer.recurring-transaction-new.account-type.INCOME={0}|Income|{1} +financer.recurring-transaction-new.account-type.LIABILITY={0}|Liability|{1} +financer.recurring-transaction-new.account-type.EXPENSE={0}|Expense|{1} +financer.recurring-transaction-new.account-type.START={0}|Start|{1} + +financer.recurring-transaction-list.title.dueToday=financer\: recurring transactions due today +financer.recurring-transaction-list.title.active=financer\: active recurring transactions +financer.recurring-transaction-list.title.all=financer\: all recurring transaction +financer.recurring-transaction-list.table-header.id=ID +financer.recurring-transaction-list.table-header.fromAccount=From account +financer.recurring-transaction-list.table-header.toAccount=To account +financer.recurring-transaction-list.table-header.firstOccurrence=First occurrence +financer.recurring-transaction-list.table-header.lastOccurrence=Last occurrence +financer.recurring-transaction-list.table-header.amount=Amount +financer.recurring-transaction-list.table-header.description=Description +financer.recurring-transaction-list.table-header.intervalType=Interval +financer.recurring-transaction-list.table-header.holidayWeekendType=Holiday/weekend rule +financer.recurring-transaction-list.table-header.actions=Actions +financer.recurring-transaction-list.table.actions.createTransaction=Create transaction +financer.recurring-transaction-list.table.actions.createTransactionWithAmount=Create transaction with amount +financer.recurring-transaction-list.table.actions.editRecurringTransaction=Edit +financer.recurring-transaction-list.table.actions.deleteRecurringTransaction=Delete + +financer.account-details.title=financer\: account details +financer.account-details.available-actions=Available actions\: +financer.account-details.available-actions.close-account=Close account +financer.account-details.available-actions.open-account=Open account +financer.account-details.table-header.id=ID +financer.account-details.table-header.fromAccount=From account +financer.account-details.table-header.toAccount=To account +financer.account-details.table-header.date=Date +financer.account-details.table-header.amount=Amount +financer.account-details.table-header.description=Description +financer.account-details.table-header.byRecurring=Recurring? +financer.account-details.details.type=Type\: +financer.account-details.details.balance=Current Balance\: +financer.account-details.table-header.actions=Actions +financer.account-details.table.actions.editTransaction=Edit +financer.account-details.table.actions.deleteTransaction=Delete + +financer.interval-type.DAILY=Daily +financer.interval-type.WEEKLY=Weekly +financer.interval-type.MONTHLY=Monthly +financer.interval-type.QUARTERLY=Quarterly +financer.interval-type.YEARLY=Yearly + +financer.holiday-weekend-type.SAME_DAY=Same day +financer.holiday-weekend-type.NEXT_WORKDAY=Next workday +financer.holiday-weekend-type.PREVIOUS_WORKDAY=Previous workday + +financer.account-type.BANK=Bank +financer.account-type.CASH=Cash +financer.account-type.INCOME=Income +financer.account-type.LIABILITY=Liability +financer.account-type.EXPENSE=Expense +financer.account-type.START=Start + +financer.account-status.OPEN=Open +financer.account-status.CLOSED=Closed + +financer.heading.transaction-new=financer\: create new transaction +financer.heading.recurring-transaction-new=financer\: create new recurring transaction +financer.heading.account-new=financer\: create new account +financer.heading.account-overview=financer\: overview +financer.heading.account-details=financer\: account details for {0} +financer.heading.recurring-transaction-list.dueToday=financer\: recurring transactions due today +financer.heading.recurring-transaction-list.active=financer\: active recurring transactions +financer.heading.recurring-transaction-list.all=financer\: all recurring transaction + +financer.error-message.UNKNOWN_ERROR=An unknown error occurred! +financer.error-message.INVALID_ACCOUNT_TYPE=The selected account type is not valid! +financer.error-message.INVALID_ACCOUNT_KEY=The entered account key is invalid! It has to start with 'accounts.'. +financer.error-message.FROM_ACCOUNT_NOT_FOUND=The specified from account has not been found! +financer.error-message.TO_ACCOUNT_NOT_FOUND=The specified to account has not been found! +financer.error-message.FROM_AND_TO_ACCOUNT_NOT_FOUND=Neither from nor to have not been found! +financer.error-message.INVALID_DATE_FORMAT=The date entered has the wrong format! +financer.error-message.MISSING_DATE=No date entered! +financer.error-message.AMOUNT_ZERO=Zero is not a valid booking amount! +financer.error-message.MISSING_AMOUNT=No amount entered! +financer.error-message.INVALID_BOOKING_ACCOUNTS=The booking is not valid! No booking allowed from the from account type to the to account type! +financer.error-message.MISSING_HOLIDAY_WEEKEND_TYPE=No holiday weekend type entered! +financer.error-message.INVALID_HOLIDAY_WEEKEND_TYPE=The holiday weekend type is not valid! +financer.error-message.MISSING_INTERVAL_TYPE=No interval type entered! +financer.error-message.INVALID_INTERVAL_TYPE=The interval type is not valid! +financer.error-message.MISSING_FIRST_OCCURRENCE=No first occurrence entered! +financer.error-message.INVALID_FIRST_OCCURRENCE_FORMAT=The date format of the first occurrence is invalid! +financer.error-message.INVALID_LAST_OCCURRENCE_FORMAT=The date format of the last occurrence is invalid! +financer.error-message.MISSING_RECURRING_TRANSACTION_ID=No recurring transaction entered! +financer.error-message.INVALID_RECURRING_TRANSACTION_ID=The recurring transaction is not valid! +financer.error-message.RECURRING_TRANSACTION_NOT_FOUND=The recurring transaction could not be found! +financer.error-message.MISSING_TRANSACTION_ID=No transaction entered! +financer.error-message.INVALID_TRANSACTION_ID=The transaction is not valid! +financer.error-message.TRANSACTION_NOT_FOUND=The transaction could not be found! +financer.error-message.ACCOUNT_NOT_FOUND=The account could not be found! \ No newline at end of file diff --git a/src/main/resources/i18n/message_de_DE.properties b/src/main/resources/i18n/message_de_DE.properties new file mode 100644 index 0000000..045cf67 --- /dev/null +++ b/src/main/resources/i18n/message_de_DE.properties @@ -0,0 +1,116 @@ +financer.account-overview.title=financer\: \u00DCbersicht +financer.account-overview.available-actions=Verf\u00FCgbare Aktionen\: +financer.account-overview.available-actions.show-closed=Zeige auch geschlossene Konten +financer.account-overview.available-actions.hide-closed=Verstecke geschlossene Konten +financer.account-overview.available-actions.create-account=Neues Konto erstellen +financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen +financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen +financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen +financer.account-overview.status=Status\: +financer.account-overview.status.recurring-transaction-due-today=Wiederkehrende Buchungen heute f\u00E4llig\: +financer.account-overview.status.recurring-transaction-active=Aktive wiederkehrende Buchungen\: +financer.account-overview.table-header.id=ID +financer.account-overview.table-header.key=Schl\u00FCssel +financer.account-overview.table-header.balance=Kontostand +financer.account-overview.table-header.type=Typ +financer.account-overview.table-header.status=Status + +financer.account-new.title=financer\: Neues Konto erstellen +financer.account-new.label.key=Schl\u00FCssel\: +financer.account-new.label.type=Typ\: +financer.account-new.submit=Account erstellen + +financer.transaction-new.title=financer\: Neue Buchung erstellen +financer.transaction-new.label.from-account=Von Konto\: +financer.transaction-new.label.to-account=An Konto\: +financer.transaction-new.label.amount=Betrag\: +financer.transaction-new.label.date=Datum\: +financer.transaction-new.label.description=Beschreibung\: +financer.transaction-new.submit=Buchung erstellen +financer.transaction-new.account-type.BANK={0}|Bank|{1} +financer.transaction-new.account-type.CASH={0}|Bar|{1} +financer.transaction-new.account-type.INCOME={0}|Einkommen|{1} +financer.transaction-new.account-type.LIABILITY={0}|Verbindlichkeit|{1} +financer.transaction-new.account-type.EXPENSE={0}|Aufwand|{1} +financer.transaction-new.account-type.START={0}|Anfangsbestand|{1} + +financer.recurring-transaction-new.title=financer\: Neue wiederkehrende Buchung erstellen +financer.recurring-transaction-new.label.from-account=Von Konto\: +financer.recurring-transaction-new.label.to-account=An Konto\: +financer.recurring-transaction-new.label.amount=Betrag\: +financer.recurring-transaction-new.label.first-occurrence=Erstes Auftreten\: +financer.recurring-transaction-new.label.last-occurrence=Letztes Auftreten\: +financer.recurring-transaction-new.label.interval-type=Intervall\: +financer.recurring-transaction-new.label.holiday-weekend-type=Feiertag-/Wochenendregel\: +financer.recurring-transaction-new.label.description=Beschreibung\: +financer.recurring-transaction-new.submit=Wiederkehrende Buchung erstellen +financer.recurring-transaction-new.account-type.BANK={0}|Bank|{1} +financer.recurring-transaction-new.account-type.CASH={0}|Bar|{1} +financer.recurring-transaction-new.account-type.INCOME={0}|Einkommen|{1} +financer.recurring-transaction-new.account-type.LIABILITY={0}|Verbindlichkeit|{1} +financer.recurring-transaction-new.account-type.EXPENSE={0}|Aufwand|{1} +financer.recurring-transaction-new.account-type.START={0}|Anfangsbestand|{1} + +financer.recurring-transaction-list.title.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig +financer.recurring-transaction-list.title.active=financer\: aktive wiederkehrende Buchungen +financer.recurring-transaction-list.title.all=financer\: alle wiederkehrende Buchungen +financer.recurring-transaction-list.table-header.id=ID +financer.recurring-transaction-list.table-header.fromAccount=Von Konto +financer.recurring-transaction-list.table-header.toAccount=An Konto +financer.recurring-transaction-list.table-header.firstOccurrence=Erstes Auftreten +financer.recurring-transaction-list.table-header.lastOccurrence=Letztes Auftreten +financer.recurring-transaction-list.table-header.amount=Betrag +financer.recurring-transaction-list.table-header.description=Beschreibung +financer.recurring-transaction-list.table-header.intervalType=Intervall +financer.recurring-transaction-list.table-header.holidayWeekendType=Feiertag-/Wochenendregel +financer.recurring-transaction-list.table-header.actions=Aktionen +financer.recurring-transaction-list.table.actions.createTransaction=Erstelle Buchung +financer.recurring-transaction-list.table.actions.createTransactionWithAmount=Erstelle Buchung mit Betrag +financer.recurring-transaction-list.table.actions.editRecurringTransaction=Bearbeiten +financer.recurring-transaction-list.table.actions.deleteRecurringTransaction=Löschen + +financer.account-details.title=financer\: Kontodetails +financer.account-details.available-actions=Verf\u00FCgbare Aktionen\: +financer.account-details.available-actions.close-account=Konto schlie\u00DFen +financer.account-details.available-actions.open-account=Konto \u00F6ffnen +financer.account-details.table-header.id=ID +financer.account-details.table-header.fromAccount=Von Konto +financer.account-details.table-header.toAccount=An Konto +financer.account-details.table-header.date=Datum +financer.account-details.table-header.amount=Betrag +financer.account-details.table-header.description=Beschreibung +financer.account-details.table-header.byRecurring=Wiederkehrend? +financer.account-details.details.type=Typ\: +financer.account-details.details.balance=Kontostand\: +financer.account-details.table-header.actions=Aktionen +financer.account-details.table.actions.editTransaction=Bearbeiten +financer.account-details.table.actions.deleteTransaction=Löschen + +financer.interval-type.DAILY=T\u00E4glich +financer.interval-type.WEEKLY=W\u00F6chentlich +financer.interval-type.MONTHLY=Monatlich +financer.interval-type.QUARTERLY=Viertelj\u00E4hrlich +financer.interval-type.YEARLY=J\u00E4hrlich + +financer.holiday-weekend-type.SAME_DAY=Gleicher Tag +financer.holiday-weekend-type.NEXT_WORKDAY=N\u00E4chster Werktag +financer.holiday-weekend-type.PREVIOUS_WORKDAY=Vorheriger Werktag + +financer.account-type.BANK=Bank +financer.account-type.CASH=Bar +financer.account-type.INCOME=Einkommen +financer.account-type.LIABILITY=Verbindlichkeit +financer.account-type.EXPENSE=Aufwand +financer.account-type.START=Anfangsbestand + +financer.account-status.OPEN=Offen +financer.account-status.CLOSED=Geschlossen + +financer.heading.transaction-new=financer\: Neue Buchung erstellen +financer.heading.recurring-transaction-new=financer\: Neue wiederkehrende Buchung erstellen +financer.heading.account-new=financer\: Neues Konto erstellen +financer.heading.account-overview=financer\: \u00DCbersicht +financer.heading.account-details=financer\: Kontodetails f\u00FCr {0} +financer.heading.recurring-transaction-list.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig +financer.heading.recurring-transaction-list.active=financer\: aktive wiederkehrende Buchungen +financer.heading.recurring-transaction-list.all=financer\: alle wiederkehrende Buchungen \ No newline at end of file diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 0000000..eb6b2d9 --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,67 @@ +#account-overview-table, +#account-transaction-table, +#recurring-transaction-list-table { + width: 100%; + border-collapse: collapse; + text-align: left; + margin-top: 1.5em; + margin-bottom: 1.5em; +} + +#account-overview-table th, +#account-overview-table td, +#account-transaction-table th, +#account-transaction-table td, +#recurring-transaction-list-table th, +#recurring-transaction-list-table td { + border-bottom: 1px solid #ddd; + padding: 0.3em; + vertical-align: top; +} + +tr:hover { + background-color: lightgrey; +} + +@media only screen and (max-width: 450px) { + .hideable-column { + display: none; + } + #new-account-form *, + #new-transaction-form *, + #new-recurring-transaction-form * { + width: 100% !important; + } +} + +#action-container *, +#recurring-transaction-list-table-actions-container *, +#account-transaction-table-actions-container * { + display: block; +} + +#details-container, +#status-container { + margin-bottom: 1em; +} + +#status-container > span, div { + display: block; +} + +#details-container > div { + display: block; +} + +#new-account-form *, +#new-transaction-form *, +#new-recurring-transaction-form * { + display: block; + margin-top: 1em; + width: 20em; + box-sizing: border-box; +} + +.errorMessage { + color: #ff6666 +} \ No newline at end of file diff --git a/src/main/resources/templates/account/accountDetails.html b/src/main/resources/templates/account/accountDetails.html new file mode 100644 index 0000000..e721517 --- /dev/null +++ b/src/main/resources/templates/account/accountDetails.html @@ -0,0 +1,59 @@ + + + + + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> +</head> +<body> +<h1 th:text="#{financer.heading.account-details(${account.key})}" /> +<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/> +<div id="details-container"> + <div id="type-container"> + <span th:text="#{financer.account-details.details.type}"/> + <span th:text="#{'financer.account-type.' + ${account.type}}"/> + </div> + <div id="balance-container"> + <span th:text="#{financer.account-details.details.balance}"/> + <span th:text="${#numbers.formatDecimal(account.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/> + </div> +</div> +<div id="action-container"> + <span th:text="#{financer.account-details.available-actions}"/> + <a th:if="${!isClosed}" th:href="@{/closeAccount(key=${account.key})}" + th:text="#{financer.account-details.available-actions.close-account}"/> + <a th:if="${isClosed}" th:href="@{/openAccount(key=${account.key})}" + th:text="#{financer.account-details.available-actions.open-account}"/> +</div> +<table id="account-transaction-table"> + <tr> + <th class="hideable-column" th:text="#{financer.account-details.table-header.id}"/> + <th th:text="#{financer.account-details.table-header.fromAccount}"/> + <th th:text="#{financer.account-details.table-header.toAccount}"/> + <th th:text="#{financer.account-details.table-header.date}"/> + <th th:text="#{financer.account-details.table-header.amount}"/> + <th th:text="#{financer.account-details.table-header.description}"/> + <th th:text="#{financer.account-details.table-header.byRecurring}"/> + <th th:text="#{financer.account-details.table-header.actions}"/> + </tr> + <tr th:each="transaction : ${transactions}"> + <td class="hideable-column" th:text="${transaction.id}"/> + <td th:text="${transaction.fromAccount.key}" /> + <td th:text="${transaction.toAccount.key}" /> + <td th:text="${transaction.date}" /> + <td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/> + <td th:text="${transaction.description}" /> + <td th:text="${transaction.recurringTransaction != null}" /> + <td> + <div id="account-transaction-table-actions-container"> + <a th:href="@{/editTransaction}" + th:text="#{financer.account-details.table.actions.editTransaction}"/> + <a th:href="@{/deleteTransaction}" + th:text="#{financer.account-details.table.actions.deleteTransaction}"/> + </div> + </td> + </tr> +</table> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/account/accountOverview.html b/src/main/resources/templates/account/accountOverview.html new file mode 100644 index 0000000..559e24d --- /dev/null +++ b/src/main/resources/templates/account/accountOverview.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{financer.account-overview.title}"/> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> +</head> +<body> +<h1 th:text="#{financer.heading.account-overview}" /> +<div id="status-container"> + <span th:text="#{financer.account-overview.status}"/> + <div> + <span th:text="#{financer.account-overview.status.recurring-transaction-due-today}"/> + <a th:href="@{/recurringTransactionDueToday}" th:text="${rtDueTodayCount}"/> + </div> + <div> + <span th:text="#{financer.account-overview.status.recurring-transaction-active}"/> + <a th:href="@{/recurringTransactionActive}" th:text="${rtAllActiveCount}"/> + </div> +</div> +<div id="action-container"> + <span th:text="#{financer.account-overview.available-actions}"/> + <a th:if="${!showClosed}" th:href="@{'/accountOverview?showClosed=true'}" + th:text="#{financer.account-overview.available-actions.show-closed}"/> + <a th:if="${showClosed}" th:href="@{'/accountOverview'}" + th:text="#{financer.account-overview.available-actions.hide-closed}"/> + <a th:href="@{/newAccount}" th:text="#{financer.account-overview.available-actions.create-account}"/> + <a th:href="@{/newTransaction}" th:text="#{financer.account-overview.available-actions.create-transaction}"/> + <a th:href="@{/newRecurringTransaction}" + th:text="#{financer.account-overview.available-actions.create-recurring-transaction}"/> + <a th:href="@{/recurringTransactionAll}" + th:text="#{financer.account-overview.available-actions.recurring-transaction-all}"/> +</div> +<table id="account-overview-table"> + <tr> + <th class="hideable-column" th:text="#{financer.account-overview.table-header.id}"/> + <th th:text="#{financer.account-overview.table-header.key}"/> + <th th:text="#{financer.account-overview.table-header.balance}"/> + <th class="hideable-column" th:text="#{financer.account-overview.table-header.type}"/> + <th class="hideable-column" th:text="#{financer.account-overview.table-header.status}"/> + </tr> + <tr th:each="acc : ${accounts}"> + <td class="hideable-column" th:text="${acc.id}"/> + <td> + <a th:href="@{/accountDetails(key=${acc.key})}" th:text="${acc.key}"/> + </td> + <td th:text="${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/> + <td class="hideable-column" th:text="#{'financer.account-type.' + ${acc.type}}"/> + <td class="hideable-column" th:text="#{'financer.account-status.' + ${acc.status}}"/> + </tr> +</table> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/account/newAccount.html b/src/main/resources/templates/account/newAccount.html new file mode 100644 index 0000000..c046626 --- /dev/null +++ b/src/main/resources/templates/account/newAccount.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{financer.account-new.title}"/> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> +</head> +<body> +<h1 th:text="#{financer.heading.account-new}" /> +<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/> +<form id="new-account-form" action="#" th:action="@{/saveAccount}" th:object="${newAccountForm}" method="post"> + <label for="inputKey" th:text="#{financer.account-new.label.key}"/> + <input type="text" id="inputKey" th:field="*{key}" placeholder="accounts."/> + <label for="selectType" th:text="#{financer.account-new.label.type}"/> + <select id="selectType" th:field="*{type}"> + <option th:each="type : ${accounttypes}" th:value="${type}" th:text="#{'financer.account-type.' + ${type}}"/> + </select> + <input type="submit" th:value="#{financer.account-new.submit}" /> +</form> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html b/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html new file mode 100644 index 0000000..6ddd215 --- /dev/null +++ b/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{financer.recurring-transaction-new.title}"/> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> +</head> +<body> +<h1 th:text="#{financer.heading.recurring-transaction-new}" /> +<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/> +<form id="new-recurring-transaction-form" action="#" th:action="@{/saveRecurringTransaction}" th:object="${newRecurringTransactionForm}" + method="post"> + <label for="selectFromAccount" th:text="#{financer.recurring-transaction-new.label.from-account}"/> + <select id="selectFromAccount" th:field="*{fromAccountKey}"> + <option th:each="acc : ${accounts}" th:value="${acc.key}" + th:text="#{'financer.recurring-transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/> + </select> + <label for="selectToAccount" th:text="#{financer.recurring-transaction-new.label.to-account}"/> + <select id="selectToAccount" th:field="*{toAccountKey}"> + <option th:each="acc : ${accounts}" th:value="${acc.key}" + th:text="#{'financer.recurring-transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/> + </select> + <label for="inputAmount" th:text="#{financer.recurring-transaction-new.label.amount}"/> + <input type="text" id="inputAmount" th:field="*{amount}"/> + <label for="inputFirstOccurrence" th:text="#{financer.recurring-transaction-new.label.first-occurrence}"/> + <input type="date" id="inputFirstOccurrence" th:field="*{firstOccurrence}"/> + <label for="inputLastOccurrence" th:text="#{financer.recurring-transaction-new.label.last-occurrence}"/> + <input type="date" id="inputLastOccurrence" th:field="*{lastOccurrence}"/> + <label for="selectInterval" th:text="#{financer.recurring-transaction-new.label.interval-type}"/> + <select id="selectInterval" th:field="*{intervalType}"> + <option th:each="inter : ${intervalTypes}" th:value="${inter}" + th:text="#{'financer.interval-type.' + ${inter}}"/> + </select> + <label for="selectHolidayWeekend" th:text="#{financer.recurring-transaction-new.label.holiday-weekend-type}"/> + <select id="selectHolidayWeekend" th:field="*{holidayWeekendType}"> + <option th:each="hdwt : ${holidayWeekendTypes}" th:value="${hdwt}" + th:text="#{'financer.holiday-weekend-type.' + ${hdwt}}"/> + </select> + <label for="inputDescription" th:text="#{financer.recurring-transaction-new.label.description}"/> + <input type="text" id="inputDescription" th:field="*{description}"/> + <input type="submit" th:value="#{financer.recurring-transaction-new.submit}"/> +</form> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/recurringTransaction/recurringTransactionList.html b/src/main/resources/templates/recurringTransaction/recurringTransactionList.html new file mode 100644 index 0000000..e77f81b --- /dev/null +++ b/src/main/resources/templates/recurringTransaction/recurringTransactionList.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{'financer.recurring-transaction-list.title.' + ${subTitle}}"/> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> +</head> +<body> +<h1 th:text="#{'financer.heading.recurring-transaction-list.' + ${subTitle}}"/> +<table id="recurring-transaction-list-table"> + <tr> + <th class="hideable-column" th:text="#{financer.recurring-transaction-list.table-header.id}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.fromAccount}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.toAccount}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.firstOccurrence}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.lastOccurrence}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.amount}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.description}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.intervalType}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.holidayWeekendType}"/> + <th th:text="#{financer.recurring-transaction-list.table-header.actions}"/> + </tr> + <tr th:each="rt : ${recurringTransactions}"> + <td class="hideable-column" th:text="${rt.id}"/> + <td> + <a th:href="@{/accountDetails(key=${rt.fromAccount.key})}" th:text="${rt.fromAccount.key}"/> + </td> + <td> + <a th:href="@{/accountDetails(key=${rt.toAccount.key})}" th:text="${rt.toAccount.key}"/> + </td> + <td th:text="${rt.firstOccurrence}"/> + <td th:text="${rt.lastOccurrence}"/> + <td th:text="${#numbers.formatDecimal(rt.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT')}"/> + <td th:text="${rt.description}"/> + <td th:text="${rt.intervalType}"/> + <td th:text="${rt.holidayWeekendType}"/> + <td> + <div id="recurring-transaction-list-table-actions-container"> + <a th:href="@{/recurringToTransaction}" + th:text="#{financer.recurring-transaction-list.table.actions.createTransaction}"/> + <a th:href="@{/recurringToTransactionWithAmount}" + th:text="#{financer.recurring-transaction-list.table.actions.createTransactionWithAmount}"/> + <a th:href="@{/editRecurringTransaction}" + th:text="#{financer.recurring-transaction-list.table.actions.editRecurringTransaction}"/> + <a th:href="@{/deleteRecurringTransaction}" + th:text="#{financer.recurring-transaction-list.table.actions.deleteRecurringTransaction}"/> + </div> + </td> + </tr> +</table> +</body> +</html> \ No newline at end of file diff --git a/src/main/resources/templates/transaction/newTransaction.html b/src/main/resources/templates/transaction/newTransaction.html new file mode 100644 index 0000000..5e6d29f --- /dev/null +++ b/src/main/resources/templates/transaction/newTransaction.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html xmlns:th="http://www.thymeleaf.org"> +<head> + <title th:text="#{financer.transaction-new.title}"/> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" th:href="@{/css/main.css}"> +</head> +<body> +<h1 th:text="#{financer.heading.transaction-new}" /> +<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/> +<form id="new-transaction-form" action="#" th:action="@{/saveTransaction}" th:object="${newTransactionForm}" + method="post"> + <label for="selectFromAccount" th:text="#{financer.transaction-new.label.from-account}"/> + <select id="selectFromAccount" th:field="*{fromAccountKey}"> + <option th:each="acc : ${accounts}" th:value="${acc.key}" + th:text="#{'financer.transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/> + </select> + <label for="selectToAccount" th:text="#{financer.transaction-new.label.to-account}"/> + <select id="selectToAccount" th:field="*{toAccountKey}"> + <option th:each="acc : ${accounts}" th:value="${acc.key}" + th:text="#{'financer.transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')})}"/> + </select> + <label for="inputAmount" th:text="#{financer.transaction-new.label.amount}"/> + <input type="text" id="inputAmount" th:field="*{amount}"/> + <label for="inputDate" th:text="#{financer.transaction-new.label.date}"/> + <input type="date" id="inputDate" th:field="*{date}"/> + <label for="inputDescription" th:text="#{financer.transaction-new.label.description}"/> + <input type="text" id="inputDescription" th:field="*{description}"/> + <input type="submit" th:value="#{financer.transaction-new.submit}"/> +</form> +</body> +</html> \ No newline at end of file diff --git a/src/test/java/de/financer/FinancerApplicationBootTest.java b/src/test/java/de/financer/FinancerApplicationBootTest.java new file mode 100644 index 0000000..21ec49f --- /dev/null +++ b/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/src/test/resources/application-integrationtest.properties b/src/test/resources/application-integrationtest.properties new file mode 100644 index 0000000..1fcad4a --- /dev/null +++ b/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