diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index e08023aad..000000000 Binary files a/.gitlab-ci.yml and /dev/null differ diff --git a/Dockerfile b/Dockerfile index 128d5b9ce..4f4eb221e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM tomcat:9-jre8 +FROM tomcat:9-jdk11 RUN rm -rf /usr/local/tomcat/webapps/* -COPY build/2.4.5/BudgetMaster-v2.4.5.war $CATALINA_HOME/webapps/ROOT.war +COPY build/2.5.0/BudgetMaster-v2.5.0.war $CATALINA_HOME/webapps/ROOT.war COPY src/main/resources/config/templates/settings-docker.properties /root/.Deadlocker/BudgetMaster/settings.properties EXPOSE 8080 \ No newline at end of file diff --git a/README.md b/README.md index 0ce08f5c9..821e6792b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Manage your monthly budget easily with BudgetMaster - __start:__ 17.12.16 -- __current release:__ v2.4.5 (28) from 19.08.20 +- __current release:__ v2.5.0 (29) from 03.12.20 ## Key Features - Keep your data private - Host your own BudgetMaster server or use it in standalone mode. All data remains on your machines. @@ -16,7 +16,7 @@ Manage your monthly budget easily with BudgetMaster - Password protected website - Your data can only be accessed by entering the correct password. (Note: The database is not encrypted) - Localization - English and German supported. - Search and Filter - Search for individual transactions or filter your view. -- Visualize your data - Use on of the pre-defined charts or create your one by using the chart framework to visualize and analyze your data. +- Visualize your data - Use one of the pre-defined charts or create your one by using the chart framework to visualize and analyze your data. - Auto Backup - Schedule an automatic export of your database content. ## Available Languages diff --git a/build/logo/BudgetMaster Icon.blend b/build/logo/BudgetMaster Icon.blend deleted file mode 100644 index af1b14927..000000000 Binary files a/build/logo/BudgetMaster Icon.blend and /dev/null differ diff --git a/build/logo/Font.txt b/build/logo/Font.txt deleted file mode 100644 index 5a083d70f..000000000 --- a/build/logo/Font.txt +++ /dev/null @@ -1 +0,0 @@ -League Gothic \ No newline at end of file diff --git a/build/screenshots/dark/accounts.png b/build/screenshots/dark/accounts.png index 5bdc2063f..d653779ba 100644 Binary files a/build/screenshots/dark/accounts.png and b/build/screenshots/dark/accounts.png differ diff --git a/build/screenshots/dark/categories.png b/build/screenshots/dark/categories.png index 3ee17912a..bbddde71c 100644 Binary files a/build/screenshots/dark/categories.png and b/build/screenshots/dark/categories.png differ diff --git a/build/screenshots/dark/chart_1.png b/build/screenshots/dark/chart_1.png index 69a56d6b6..dc238ce7f 100644 Binary files a/build/screenshots/dark/chart_1.png and b/build/screenshots/dark/chart_1.png differ diff --git a/build/screenshots/dark/chart_2.png b/build/screenshots/dark/chart_2.png index 5380589e9..4ef472e4b 100644 Binary files a/build/screenshots/dark/chart_2.png and b/build/screenshots/dark/chart_2.png differ diff --git a/build/screenshots/dark/chart_3.png b/build/screenshots/dark/chart_3.png index a6ef90957..a6a9458b9 100644 Binary files a/build/screenshots/dark/chart_3.png and b/build/screenshots/dark/chart_3.png differ diff --git a/build/screenshots/dark/chart_4.png b/build/screenshots/dark/chart_4.png index 9d6ddf292..ea9de7795 100644 Binary files a/build/screenshots/dark/chart_4.png and b/build/screenshots/dark/chart_4.png differ diff --git a/build/screenshots/dark/filter_1.png b/build/screenshots/dark/filter_1.png index f3766d2bd..ae5f87d86 100644 Binary files a/build/screenshots/dark/filter_1.png and b/build/screenshots/dark/filter_1.png differ diff --git a/build/screenshots/dark/filter_2.png b/build/screenshots/dark/filter_2.png index be19f7e56..cad992f60 100644 Binary files a/build/screenshots/dark/filter_2.png and b/build/screenshots/dark/filter_2.png differ diff --git a/build/screenshots/dark/home.png b/build/screenshots/dark/home.png index 45a8b5db6..6a05cee43 100644 Binary files a/build/screenshots/dark/home.png and b/build/screenshots/dark/home.png differ diff --git a/build/screenshots/dark/hotkeys.png b/build/screenshots/dark/hotkeys.png index d8a673807..eda6c5de1 100644 Binary files a/build/screenshots/dark/hotkeys.png and b/build/screenshots/dark/hotkeys.png differ diff --git a/build/screenshots/dark/new_category.png b/build/screenshots/dark/new_category.png index 6865b5ac5..e4d659dba 100644 Binary files a/build/screenshots/dark/new_category.png and b/build/screenshots/dark/new_category.png differ diff --git a/build/screenshots/dark/new_normal_transaction.png b/build/screenshots/dark/new_normal_transaction.png index f1a6f9bfc..648a87fea 100644 Binary files a/build/screenshots/dark/new_normal_transaction.png and b/build/screenshots/dark/new_normal_transaction.png differ diff --git a/build/screenshots/dark/new_transaction_1.png b/build/screenshots/dark/new_transaction_1.png index 71d325577..e1341cd03 100644 Binary files a/build/screenshots/dark/new_transaction_1.png and b/build/screenshots/dark/new_transaction_1.png differ diff --git a/build/screenshots/dark/new_transaction_2.png b/build/screenshots/dark/new_transaction_2.png index 4ab0b885f..c439a06c6 100644 Binary files a/build/screenshots/dark/new_transaction_2.png and b/build/screenshots/dark/new_transaction_2.png differ diff --git a/build/screenshots/dark/new_transfer_transaction.png b/build/screenshots/dark/new_transfer_transaction.png index 3bbf17017..6da41fa11 100644 Binary files a/build/screenshots/dark/new_transfer_transaction.png and b/build/screenshots/dark/new_transfer_transaction.png differ diff --git a/build/screenshots/dark/reports.png b/build/screenshots/dark/reports.png index 7bd939708..42a6db44c 100644 Binary files a/build/screenshots/dark/reports.png and b/build/screenshots/dark/reports.png differ diff --git a/build/screenshots/dark/search.png b/build/screenshots/dark/search.png index c6583ec20..58ed42492 100644 Binary files a/build/screenshots/dark/search.png and b/build/screenshots/dark/search.png differ diff --git a/build/screenshots/dark/settings_1.png b/build/screenshots/dark/settings_1.png index 628041276..c3a0e994e 100644 Binary files a/build/screenshots/dark/settings_1.png and b/build/screenshots/dark/settings_1.png differ diff --git a/build/screenshots/dark/settings_2.png b/build/screenshots/dark/settings_2.png index 0cbb5b675..07745fcf1 100644 Binary files a/build/screenshots/dark/settings_2.png and b/build/screenshots/dark/settings_2.png differ diff --git a/build/screenshots/dark/templates_1.png b/build/screenshots/dark/templates_1.png index ee985aade..a0074e2b1 100644 Binary files a/build/screenshots/dark/templates_1.png and b/build/screenshots/dark/templates_1.png differ diff --git a/build/screenshots/dark/templates_2.png b/build/screenshots/dark/templates_2.png deleted file mode 100644 index 8f0b2d521..000000000 Binary files a/build/screenshots/dark/templates_2.png and /dev/null differ diff --git a/build/screenshots/dark/transactions.png b/build/screenshots/dark/transactions.png index 1bddd35bf..a404e4064 100644 Binary files a/build/screenshots/dark/transactions.png and b/build/screenshots/dark/transactions.png differ diff --git a/build/screenshots/light/accounts.png b/build/screenshots/light/accounts.png index b2ec96479..ba4183b6f 100644 Binary files a/build/screenshots/light/accounts.png and b/build/screenshots/light/accounts.png differ diff --git a/build/screenshots/light/categories.png b/build/screenshots/light/categories.png index 54d7ba010..94a36483d 100644 Binary files a/build/screenshots/light/categories.png and b/build/screenshots/light/categories.png differ diff --git a/build/screenshots/light/chart_1.png b/build/screenshots/light/chart_1.png index fc360eca5..1dff6f9f1 100644 Binary files a/build/screenshots/light/chart_1.png and b/build/screenshots/light/chart_1.png differ diff --git a/build/screenshots/light/chart_2.png b/build/screenshots/light/chart_2.png index a12d8ab09..c6cb8ab78 100644 Binary files a/build/screenshots/light/chart_2.png and b/build/screenshots/light/chart_2.png differ diff --git a/build/screenshots/light/chart_3.png b/build/screenshots/light/chart_3.png index ce95eca27..a2133e80c 100644 Binary files a/build/screenshots/light/chart_3.png and b/build/screenshots/light/chart_3.png differ diff --git a/build/screenshots/light/chart_4.png b/build/screenshots/light/chart_4.png index c3ecd69d2..b440c9c6f 100644 Binary files a/build/screenshots/light/chart_4.png and b/build/screenshots/light/chart_4.png differ diff --git a/build/screenshots/light/filter_1.png b/build/screenshots/light/filter_1.png index 1d0cc77e8..2dee371ac 100644 Binary files a/build/screenshots/light/filter_1.png and b/build/screenshots/light/filter_1.png differ diff --git a/build/screenshots/light/filter_2.png b/build/screenshots/light/filter_2.png index 87548a7ed..eeea2d8da 100644 Binary files a/build/screenshots/light/filter_2.png and b/build/screenshots/light/filter_2.png differ diff --git a/build/screenshots/light/home.png b/build/screenshots/light/home.png index 4d644578d..9580d5955 100644 Binary files a/build/screenshots/light/home.png and b/build/screenshots/light/home.png differ diff --git a/build/screenshots/light/hotkeys.png b/build/screenshots/light/hotkeys.png index 48b1724cf..48c5985ac 100644 Binary files a/build/screenshots/light/hotkeys.png and b/build/screenshots/light/hotkeys.png differ diff --git a/build/screenshots/light/new_category.png b/build/screenshots/light/new_category.png index 7f073b2fd..1d26edf20 100644 Binary files a/build/screenshots/light/new_category.png and b/build/screenshots/light/new_category.png differ diff --git a/build/screenshots/light/new_normal_transaction.png b/build/screenshots/light/new_normal_transaction.png index 66edaa908..ba49581ee 100644 Binary files a/build/screenshots/light/new_normal_transaction.png and b/build/screenshots/light/new_normal_transaction.png differ diff --git a/build/screenshots/light/new_transaction_1.png b/build/screenshots/light/new_transaction_1.png index 26889c76f..13940d6a3 100644 Binary files a/build/screenshots/light/new_transaction_1.png and b/build/screenshots/light/new_transaction_1.png differ diff --git a/build/screenshots/light/new_transaction_2.png b/build/screenshots/light/new_transaction_2.png index 14a434d8c..39c45b824 100644 Binary files a/build/screenshots/light/new_transaction_2.png and b/build/screenshots/light/new_transaction_2.png differ diff --git a/build/screenshots/light/new_transfer_transaction.png b/build/screenshots/light/new_transfer_transaction.png index 2eec50146..b54cf43ee 100644 Binary files a/build/screenshots/light/new_transfer_transaction.png and b/build/screenshots/light/new_transfer_transaction.png differ diff --git a/build/screenshots/light/reports.png b/build/screenshots/light/reports.png index 23273e986..83f10b217 100644 Binary files a/build/screenshots/light/reports.png and b/build/screenshots/light/reports.png differ diff --git a/build/screenshots/light/search.png b/build/screenshots/light/search.png index 6ebc590b1..5ee707c97 100644 Binary files a/build/screenshots/light/search.png and b/build/screenshots/light/search.png differ diff --git a/build/screenshots/light/settings_1.png b/build/screenshots/light/settings_1.png index cd1c2a3fd..ee2d86f10 100644 Binary files a/build/screenshots/light/settings_1.png and b/build/screenshots/light/settings_1.png differ diff --git a/build/screenshots/light/settings_2.png b/build/screenshots/light/settings_2.png index 232af88e4..a55ea76d7 100644 Binary files a/build/screenshots/light/settings_2.png and b/build/screenshots/light/settings_2.png differ diff --git a/build/screenshots/light/templates_1.png b/build/screenshots/light/templates_1.png index a25ea3342..c2211e290 100644 Binary files a/build/screenshots/light/templates_1.png and b/build/screenshots/light/templates_1.png differ diff --git a/build/screenshots/light/templates_2.png b/build/screenshots/light/templates_2.png deleted file mode 100644 index e7ccbada1..000000000 Binary files a/build/screenshots/light/templates_2.png and /dev/null differ diff --git a/build/screenshots/light/transactions.png b/build/screenshots/light/transactions.png index ca39e3a06..facf1bd4a 100644 Binary files a/build/screenshots/light/transactions.png and b/build/screenshots/light/transactions.png differ diff --git a/pom.xml b/pom.xml index 9f82d2adf..fa143cb5b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.deadlocker8 BudgetMaster - 2.4.5 + 2.5.0 BudgetMaster @@ -35,7 +35,7 @@ org.springframework.boot spring-boot-starter-parent - 2.2.5.RELEASE + 2.2.11.RELEASE @@ -54,23 +54,23 @@ UTF-8 UTF-8 - 1.8 + 11 - 2.0.6 - 1.2.1 - 0.39 - 3.4.1 + 3.2.0 + 3.0.1 + 0.40 + 3.5.1 1.0.0 - 5.12.0 - 1.8.3 - 1.6.1 + 5.15.1 + 1.10.2 + 1.6.5 5.50.0 3.141.59 - 3.15.0 + 3.17.1 ${maven.build.timestamp} dd.MM.yy - 28 + 29 Robert Goldmann build/${project.version} @@ -245,7 +245,7 @@ org.apache.maven.plugins maven-war-plugin - 3.2.2 + 3.3.1 ${basedir}/src/main ${project.outputDirectory} @@ -264,7 +264,7 @@ com.akathist.maven.plugins.launch4j launch4j-maven-plugin - 1.7.21 + 1.7.25 l4j-clui @@ -283,7 +283,7 @@ false false - 1.8.0 + 11 preferJre 64/32 @@ -296,7 +296,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.12 + 2.22.1 junit:junit -Dfile.encoding=UTF-8 @@ -311,7 +311,7 @@ org.codehaus.mojo build-helper-maven-plugin - 1.10 + 1.12 attach-artifacts diff --git a/src/main/java/de/deadlocker8/budgetmaster/Main.java b/src/main/java/de/deadlocker8/budgetmaster/Main.java index 1ec0c1ab7..77401e212 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/Main.java +++ b/src/main/java/de/deadlocker8/budgetmaster/Main.java @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.text.MessageFormat; import java.util.*; @@ -47,9 +48,9 @@ public Locale getLocale() } @Override - public String getBaseResource() + public String[] getBaseResources() { - return "languages/"; + return new String[]{"languages/base", "languages/news"}; } @Override @@ -57,10 +58,17 @@ public LocalizationMessageFormatter messageFormatter() { return new JavaMessageFormatter(); } + + @Override + public boolean useMultipleResourceBundles() + { + return true; + } }); Localization.load(); ProgramArgs.setArgs(Arrays.asList(args)); + LOGGER.debug(MessageFormat.format("Starting with ProgramArgs: {0}", ProgramArgs.getArgs())); Path applicationSupportFolder = getApplicationSupportFolder(); PathUtils.createDirectoriesIfNotExists(applicationSupportFolder); @@ -106,12 +114,12 @@ else if(ProgramArgs.isDebug()) } else { - LOGGER.error("Ignoring option --customFolder: provided path '" + customFolder.toString() + "' is not absolute"); + LOGGER.error(MessageFormat.format("Ignoring option --customFolder: provided path ''{0}'' is not absolute", customFolder.toString())); } } savePath = determineFolder(savePath); - LOGGER.info("Used save path: " + savePath.toString()); + LOGGER.info(MessageFormat.format("Used save path: {0}", savePath.toString())); return savePath; } @@ -180,6 +188,6 @@ public void run(ApplicationArguments args) private static void logAppInfo(String appName, String versionName, String versionCode, String versionDate) { - LOGGER.info(appName + " - v" + versionName + " - (versioncode: " + versionCode + ") from " + versionDate + ")"); + LOGGER.info(MessageFormat.format("{0} - v{1} - (versioncode: {2}) from {3})", appName, versionName, versionCode, versionDate)); } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/ProgramArgs.java b/src/main/java/de/deadlocker8/budgetmaster/ProgramArgs.java index 3a17d593c..f36195409 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/ProgramArgs.java +++ b/src/main/java/de/deadlocker8/budgetmaster/ProgramArgs.java @@ -13,6 +13,10 @@ public class ProgramArgs private static List args = new ArrayList<>(); + private ProgramArgs() + { + } + public static void setArgs(List args) { ProgramArgs.args = args; diff --git a/src/main/java/de/deadlocker8/budgetmaster/accounts/Account.java b/src/main/java/de/deadlocker8/budgetmaster/accounts/Account.java index 0a3fdb525..fbbaa93ce 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/accounts/Account.java +++ b/src/main/java/de/deadlocker8/budgetmaster/accounts/Account.java @@ -28,6 +28,7 @@ public class Account private Boolean isSelected = false; private Boolean isDefault = false; + private Boolean isReadOnly = false; @Expose private AccountType type; @@ -39,6 +40,7 @@ public Account(String name, AccountType type) this.type = type; this.isSelected = false; this.isDefault = false; + this.isReadOnly = false; } public Account() @@ -95,6 +97,16 @@ public void setDefault(Boolean aDefault) isDefault = aDefault; } + public Boolean isReadOnly() + { + return isReadOnly; + } + + public void setReadOnly(Boolean readOnly) + { + isReadOnly = readOnly; + } + public AccountType getType() { return type; @@ -114,6 +126,7 @@ public String toString() ", referringTransactions=" + referringTransactions + ", isSelected=" + isSelected + ", isDefault=" + isDefault + + ", isReadOnly=" + isReadOnly + ", type=" + type + '}'; } @@ -126,6 +139,7 @@ public boolean equals(Object o) Account account = (Account) o; return isSelected == account.isSelected && isDefault == account.isDefault && + isReadOnly == account.isReadOnly && Objects.equals(ID, account.ID) && Objects.equals(name, account.name) && type == account.type; @@ -134,6 +148,6 @@ public boolean equals(Object o) @Override public int hashCode() { - return Objects.hash(ID, name, isSelected, isDefault, type); + return Objects.hash(ID, name, isSelected, isDefault, isReadOnly, type); } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java b/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java index df03dddd7..54cd57729 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java @@ -2,37 +2,34 @@ import de.deadlocker8.budgetmaster.controller.BaseController; import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.ResourceNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.Optional; @Controller +@RequestMapping(Mappings.ACCOUNTS) public class AccountController extends BaseController { - private final AccountRepository accountRepository; private final AccountService accountService; private final SettingsService settingsService; @Autowired - public AccountController(AccountRepository accountRepository, AccountService accountService, SettingsService settingsService) + public AccountController(AccountService accountService, SettingsService settingsService) { - this.accountRepository = accountRepository; this.accountService = accountService; this.settingsService = settingsService; } - @GetMapping(value = "/accounts/{ID}/select") + @GetMapping(value = "/{ID}/select") public String selectAccount(HttpServletRequest request, @PathVariable("ID") Integer ID) { accountService.selectAccount(ID); @@ -45,7 +42,7 @@ public String selectAccount(HttpServletRequest request, @PathVariable("ID") Inte return "redirect:" + referer; } - @GetMapping(value = "/accounts/{ID}/setAsDefault") + @GetMapping(value = "/{ID}/setAsDefault") public String setAsDefault(HttpServletRequest request, @PathVariable("ID") Integer ID) { accountService.setAsDefaultAccount(ID); @@ -58,7 +55,31 @@ public String setAsDefault(HttpServletRequest request, @PathVariable("ID") Integ return "redirect:" + referer; } - @GetMapping("/accounts") + @GetMapping(value = "/{ID}/toggleReadOnly") + public String toggleReadOnly(HttpServletRequest request, @PathVariable("ID") Integer ID) + { + final Optional accountOptional = accountService.getRepository().findById(ID); + if(accountOptional.isEmpty()) + { + throw new ResourceNotFoundException(); + } + + final Account account = accountOptional.get(); + if(!account.isDefault()) + { + account.setReadOnly(!account.isReadOnly()); + accountService.getRepository().save(account); + } + + String referer = request.getHeader("Referer"); + if(referer.contains("database/import")) + { + return "redirect:/settings"; + } + return "redirect:" + referer; + } + + @GetMapping public String accounts(Model model) { model.addAttribute("accounts", accountService.getAllAccountsAsc()); @@ -66,32 +87,32 @@ public String accounts(Model model) return "accounts/accounts"; } - @GetMapping("/accounts/{ID}/requestDelete") + @GetMapping("/{ID}/requestDelete") public String requestDeleteAccount(Model model, @PathVariable("ID") Integer ID) { model.addAttribute("accounts", accountService.getAllAccountsAsc()); - model.addAttribute("currentAccount", accountRepository.getOne(ID)); + model.addAttribute("currentAccount", accountService.getRepository().getOne(ID)); model.addAttribute("settings", settingsService.getSettings()); return "accounts/accounts"; } - @GetMapping("/accounts/{ID}/delete") + @GetMapping("/{ID}/delete") public String deleteAccountAndReferringTransactions(Model model, @PathVariable("ID") Integer ID) { - if(accountRepository.findAllByType(AccountType.CUSTOM).size() > 1) + if(accountService.getRepository().findAllByType(AccountType.CUSTOM).size() > 1) { accountService.deleteAccount(ID); return "redirect:/accounts"; } model.addAttribute("accounts", accountService.getAllAccountsAsc()); - model.addAttribute("currentAccount", accountRepository.getOne(ID)); + model.addAttribute("currentAccount", accountService.getRepository().getOne(ID)); model.addAttribute("accountNotDeletable", true); model.addAttribute("settings", settingsService.getSettings()); return "accounts/accounts"; } - @GetMapping("/accounts/newAccount") + @GetMapping("/newAccount") public String newAccount(Model model) { Account emptyAccount = new Account(); @@ -100,11 +121,11 @@ public String newAccount(Model model) return "accounts/newAccount"; } - @GetMapping("/accounts/{ID}/edit") + @GetMapping("/{ID}/edit") public String editAccount(Model model, @PathVariable("ID") Integer ID) { - Optional accountOptional = accountRepository.findById(ID); - if(!accountOptional.isPresent()) + Optional accountOptional = accountService.getRepository().findById(ID); + if(accountOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -114,7 +135,7 @@ public String editAccount(Model model, @PathVariable("ID") Integer ID) return "accounts/newAccount"; } - @PostMapping(value = "/accounts/newAccount") + @PostMapping(value = "/newAccount") public String post(HttpServletRequest request, Model model, @ModelAttribute("NewAccount") Account account, BindingResult bindingResult) @@ -122,7 +143,7 @@ public String post(HttpServletRequest request, Model model, AccountValidator accountValidator = new AccountValidator(); accountValidator.validate(account, bindingResult); - if(accountRepository.findByName(account.getName()) != null) + if(accountService.getRepository().findByName(account.getName()) != null) { bindingResult.addError(new FieldError("NewAccount", "name", "", false, new String[]{"warning.duplicate.account.name"}, null, null)); } @@ -140,17 +161,17 @@ public String post(HttpServletRequest request, Model model, if(account.getID() == null) { // new account - accountRepository.save(account); + accountService.getRepository().save(account); } else { // edit existing account - Optional existingAccountOptional = accountRepository.findById(account.getID()); + Optional existingAccountOptional = accountService.getRepository().findById(account.getID()); if(existingAccountOptional.isPresent()) { Account existingAccount = existingAccountOptional.get(); existingAccount.setName(account.getName()); - accountRepository.save(existingAccount); + accountService.getRepository().save(existingAccount); } } } diff --git a/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountRepository.java b/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountRepository.java index 01e72be7f..3536d8fb2 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountRepository.java +++ b/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountRepository.java @@ -9,6 +9,8 @@ public interface AccountRepository extends JpaRepository { List findAllByTypeOrderByNameAsc(AccountType accountType); + List findAllByTypeAndIsReadOnlyOrderByNameAsc(AccountType accountType, Boolean isReadOnly); + Account findByName(String name); List findAllByType(AccountType accountType); diff --git a/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountService.java b/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountService.java index 9f0c7c522..a2c710af8 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountService.java @@ -18,10 +18,11 @@ @Service public class AccountService implements Resetable { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); - private AccountRepository accountRepository; - private TransactionService transactionService; - private UserRepository userRepository; + private static final Logger LOGGER = LoggerFactory.getLogger(AccountService.class); + + private final AccountRepository accountRepository; + private final TransactionService transactionService; + private final UserRepository userRepository; @Autowired public AccountService(AccountRepository accountRepository, TransactionService transactionService, UserRepository userRepository) @@ -45,10 +46,17 @@ public List getAllAccountsAsc() return accounts; } + public List getAllActivatedAccountsAsc() + { + List accounts = accountRepository.findAllByType(AccountType.ALL); + accounts.addAll(accountRepository.findAllByTypeAndIsReadOnlyOrderByNameAsc(AccountType.CUSTOM, false)); + return accounts; + } + public void deleteAccount(int ID) { Optional accountToDeleteOptional = accountRepository.findById(ID); - if(!accountToDeleteOptional.isPresent()) + if(accountToDeleteOptional.isEmpty()) { return; } @@ -97,6 +105,16 @@ public void createDefaults() LOGGER.debug("Created default account"); } + // handle null values for new field "isReadOnly" + for(Account account : accountRepository.findAll()) + { + if(account.isReadOnly() == null) + { + account.setReadOnly(false); + } + accountRepository.save(account); + } + Account defaultAccount = accountRepository.findByIsDefault(true); if(defaultAccount == null) { @@ -121,7 +139,7 @@ public void selectAccount(int ID) deselectAllAccounts(); Optional accountToSelectOptional = accountRepository.findById(ID); - if(!accountToSelectOptional.isPresent()) + if(accountToSelectOptional.isEmpty()) { return; } @@ -140,15 +158,20 @@ public void selectAccount(int ID) public void setAsDefaultAccount(int ID) { - unsetDefaultForAllAccounts(); - Optional accountToSelectOptional = accountRepository.findById(ID); - if(!accountToSelectOptional.isPresent()) + if(accountToSelectOptional.isEmpty()) { return; } Account accountToSelect = accountToSelectOptional.get(); + if(accountToSelect.isReadOnly()) + { + return; + } + + unsetDefaultForAllAccounts(); + accountToSelect.setDefault(true); accountRepository.save(accountToSelect); } diff --git a/src/main/java/de/deadlocker8/budgetmaster/authentication/LoginController.java b/src/main/java/de/deadlocker8/budgetmaster/authentication/LoginController.java index c17be2d87..a6b133113 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/authentication/LoginController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/authentication/LoginController.java @@ -1,6 +1,7 @@ package de.deadlocker8.budgetmaster.authentication; import de.deadlocker8.budgetmaster.controller.BaseController; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.joda.time.DateTime; import org.springframework.security.web.savedrequest.DefaultSavedRequest; import org.springframework.stereotype.Controller; @@ -12,20 +13,25 @@ import java.util.Map; @Controller +@RequestMapping(Mappings.LOGIN) public class LoginController extends BaseController { - @GetMapping("/login") + @GetMapping public String login(HttpServletRequest request, Model model) { Map paramMap = request.getParameterMap(); if(paramMap.containsKey("error")) + { model.addAttribute("isError", true); + } if(paramMap.containsKey("logout")) + { model.addAttribute("isLogout", true); + } - DefaultSavedRequest savedRequest = (DefaultSavedRequest)request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST"); + DefaultSavedRequest savedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST"); if(savedRequest != null) { request.getSession().setAttribute("preLoginURL", savedRequest.getRequestURL()); diff --git a/src/main/java/de/deadlocker8/budgetmaster/authentication/UserService.java b/src/main/java/de/deadlocker8/budgetmaster/authentication/UserService.java index 702d3b1ca..8dc3b2cda 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/authentication/UserService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/authentication/UserService.java @@ -11,7 +11,8 @@ @Service public class UserService { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); + public static final String DEFAULT_PASSWORD = "BudgetMaster"; @Autowired diff --git a/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryController.java b/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryController.java index d824fbaed..93def3e53 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryController.java @@ -4,6 +4,7 @@ import de.deadlocker8.budgetmaster.services.HelpersService; import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.utils.Colors; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.ResourceNotFoundException; import de.thecodelabs.utils.util.ColorUtilsNonJavaFX; import org.springframework.beans.factory.annotation.Autowired; @@ -18,6 +19,7 @@ @Controller +@RequestMapping(Mappings.CATEGORIES) public class CategoryController extends BaseController { private static final String WHITE = "#FFFFFF"; @@ -34,7 +36,7 @@ public CategoryController(CategoryService categoryService, HelpersService helper this.settingsService = settingsService; } - @GetMapping("/categories") + @GetMapping public String categories(Model model) { model.addAttribute("categories", categoryService.getAllCategories()); @@ -42,7 +44,7 @@ public String categories(Model model) return "categories/categories"; } - @GetMapping("/categories/{ID}/requestDelete") + @GetMapping("/{ID}/requestDelete") public String requestDeleteCategory(Model model, @PathVariable("ID") Integer ID) { if(!categoryService.isDeletable(ID)) @@ -55,15 +57,15 @@ public String requestDeleteCategory(Model model, @PathVariable("ID") Integer ID) model.addAttribute("categories", allCategories); model.addAttribute("availableCategories", availableCategories); - model.addAttribute("preselectedCategory", categoryService.getRepository().findByType(CategoryType.NONE)); + model.addAttribute("preselectedCategory", categoryService.findByType(CategoryType.NONE)); - model.addAttribute("currentCategory", categoryService.getRepository().getOne(ID)); + model.addAttribute("currentCategory", categoryService.findById(ID).get()); model.addAttribute("settings", settingsService.getSettings()); return "categories/categories"; } - @PostMapping(value = "/categories/{ID}/delete") - public String deleteCategory(Model model, @PathVariable("ID") Integer ID, @ModelAttribute("DestinationCategory") DestinationCategory destinationCategory) + @PostMapping(value = "/{ID}/delete") + public String deleteCategory(@PathVariable("ID") Integer ID, @ModelAttribute("DestinationCategory") DestinationCategory destinationCategory) { if(categoryService.isDeletable(ID)) { @@ -73,7 +75,7 @@ public String deleteCategory(Model model, @PathVariable("ID") Integer ID, @Model return "redirect:/categories"; } - @GetMapping("/categories/newCategory") + @GetMapping("/newCategory") public String newCategory(Model model) { //add custom color (defaults to white here because we are adding a new category instead of editing an existing) @@ -84,11 +86,11 @@ public String newCategory(Model model) return "categories/newCategory"; } - @GetMapping("/categories/{ID}/edit") + @GetMapping("/{ID}/edit") public String editCategory(Model model, @PathVariable("ID") Integer ID) { - Optional categoryOptional = categoryService.getRepository().findById(ID); - if(!categoryOptional.isPresent()) + Optional categoryOptional = categoryService.findById(ID); + if(categoryOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -109,7 +111,7 @@ public String editCategory(Model model, @PathVariable("ID") Integer ID) return "categories/newCategory"; } - @PostMapping(value = "/categories/newCategory") + @PostMapping(value = "/newCategory") public String post(Model model, @ModelAttribute("NewCategory") Category category, BindingResult bindingResult) { CategoryValidator userValidator = new CategoryValidator(); @@ -142,7 +144,7 @@ public String post(Model model, @ModelAttribute("NewCategory") Category category { category.setType(CategoryType.CUSTOM); } - categoryService.getRepository().save(category); + categoryService.save(category); } return "redirect:/categories"; diff --git a/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryService.java b/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryService.java index 285a6743f..34c2ec81c 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/categories/CategoryService.java @@ -1,7 +1,6 @@ package de.deadlocker8.budgetmaster.categories; import de.deadlocker8.budgetmaster.services.Resetable; -import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.utils.Strings; import de.thecodelabs.utils.util.Localization; @@ -10,15 +9,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Comparator; import java.util.List; -import java.util.Locale; import java.util.Optional; +import java.util.stream.Collectors; @Service public class CategoryService implements Resetable { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); - private CategoryRepository categoryRepository; + private static final Logger LOGGER = LoggerFactory.getLogger(CategoryService.class); + private final CategoryRepository categoryRepository; @Autowired public CategoryService(CategoryRepository categoryRepository) @@ -28,15 +28,25 @@ public CategoryService(CategoryRepository categoryRepository) createDefaults(); } - public CategoryRepository getRepository() + public Optional findById(Integer ID) { - return categoryRepository; + return categoryRepository.findById(ID); + } + + public Category findByType(CategoryType type) + { + return categoryRepository.findByType(type); + } + + public Category save(Category category) + { + return categoryRepository.save(category); } public void deleteCategory(int ID, Category newCategory) { Optional categoryOptional = categoryRepository.findById(ID); - if(!categoryOptional.isPresent()) + if(categoryOptional.isEmpty()) { throw new RuntimeException("Can't delete non-existing category with ID: " + ID); } @@ -57,7 +67,7 @@ public void deleteCategory(int ID, Category newCategory) @SuppressWarnings("OptionalIsPresent") public boolean isDeletable(Integer ID) { - Optional categoryOptional = getRepository().findById(ID); + Optional categoryOptional = findById(ID); if(categoryOptional.isPresent()) { return categoryOptional.get().getType() == CategoryType.CUSTOM; @@ -91,7 +101,9 @@ public void createDefaults() public List getAllCategories() { localizeDefaultCategories(); - return categoryRepository.findAllByOrderByNameAsc(); + return categoryRepository.findAllByOrderByNameAsc().stream() + .sorted(Comparator.comparing(c -> c.getName().toLowerCase())) + .collect(Collectors.toList()); } public void localizeDefaultCategories() diff --git a/src/main/java/de/deadlocker8/budgetmaster/charts/ChartController.java b/src/main/java/de/deadlocker8/budgetmaster/charts/ChartController.java index 2cc0de75a..32dd04805 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/charts/ChartController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/charts/ChartController.java @@ -11,6 +11,7 @@ import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.transactions.TransactionService; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.ResourceNotFoundException; import org.joda.time.DateTime; import org.joda.time.format.ISODateTimeFormat; @@ -25,6 +26,7 @@ import java.util.UUID; @Controller +@RequestMapping(Mappings.CHARTS) public class ChartController extends BaseController { private static final Gson GSON = new GsonBuilder() @@ -49,7 +51,7 @@ public ChartController(ChartService chartService, HelpersService helpers, Settin this.transactionService = transactionService; } - @GetMapping("/charts") + @GetMapping public String charts(Model model) { List charts = chartService.getRepository().findAllByOrderByNameAsc(); @@ -66,12 +68,12 @@ public String charts(Model model) return "charts/charts"; } - @PostMapping(value = "/charts") + @PostMapping public String showChart(Model model, @ModelAttribute("NewChartSettings") ChartSettings chartSettings) { chartSettings.setFilterConfiguration(filterHelpersService.updateCategoriesAndTags(chartSettings.getFilterConfiguration())); Optional chartOptional = chartService.getRepository().findById(chartSettings.getChartID()); - if(!chartOptional.isPresent()) + if(chartOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -88,7 +90,7 @@ public String showChart(Model model, @ModelAttribute("NewChartSettings") ChartSe return "charts/charts"; } - @GetMapping("/charts/manage") + @GetMapping("/manage") public String manage(Model model) { model.addAttribute("charts", chartService.getRepository().findAllByOrderByNameAsc()); @@ -96,7 +98,7 @@ public String manage(Model model) return "charts/manage"; } - @GetMapping("/charts/newChart") + @GetMapping("/newChart") public String newChart(Model model) { Chart emptyChart = DefaultCharts.CHART_DEFAULT; @@ -105,11 +107,11 @@ public String newChart(Model model) return "charts/newChart"; } - @GetMapping("/charts/{ID}/edit") + @GetMapping("/{ID}/edit") public String editChart(Model model, @PathVariable("ID") Integer ID) { Optional chartOptional = chartService.getRepository().findById(ID); - if(!chartOptional.isPresent()) + if(chartOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -119,7 +121,7 @@ public String editChart(Model model, @PathVariable("ID") Integer ID) return "charts/newChart"; } - @PostMapping(value = "/charts/newChart") + @PostMapping(value = "/newChart") public String post(Model model, @ModelAttribute("NewChart") Chart chart, BindingResult bindingResult) { ChartValidator userValidator = new ChartValidator(); @@ -165,7 +167,7 @@ public String post(Model model, @ModelAttribute("NewChart") Chart chart, Binding return "redirect:/charts/manage"; } - @GetMapping("/charts/{ID}/requestDelete") + @GetMapping("/{ID}/requestDelete") public String requestDeleteChart(Model model, @PathVariable("ID") Integer ID) { if(!chartService.isDeletable(ID)) @@ -179,7 +181,7 @@ public String requestDeleteChart(Model model, @PathVariable("ID") Integer ID) return "charts/manage"; } - @GetMapping(value = "/charts/{ID}/delete") + @GetMapping(value = "/{ID}/delete") public String deleteChart(Model model, @PathVariable("ID") Integer ID) { if(chartService.isDeletable(ID)) diff --git a/src/main/java/de/deadlocker8/budgetmaster/charts/ChartService.java b/src/main/java/de/deadlocker8/budgetmaster/charts/ChartService.java index 8eefb4573..5b97ebc34 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/charts/ChartService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/charts/ChartService.java @@ -6,15 +6,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.text.MessageFormat; import java.util.List; import java.util.Optional; @Service public class ChartService implements Resetable { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); - private final String PATTERN_OLD_CONTAINER_ID = "Plotly.newPlot('chart-canvas',"; - private final String PATTERN_DYNAMIC_CONTAINER_ID = "Plotly.newPlot('containerID',"; + private static final Logger LOGGER = LoggerFactory.getLogger(ChartService.class); + + private static final String PATTERN_OLD_CONTAINER_ID = "Plotly.newPlot('chart-canvas',"; + private static final String PATTERN_DYNAMIC_CONTAINER_ID = "Plotly.newPlot('containerID',"; private ChartRepository chartRepository; @@ -60,11 +62,11 @@ public void createDefaults() { chart.setID(defaultCharts.indexOf(chart) + 1); chartRepository.save(chart); - LOGGER.debug("Created default chart '" + chart.getName() + "'"); + LOGGER.debug(MessageFormat.format("Created default chart ''{0}''", chart.getName())); } else if(currentChart.getVersion() < chart.getVersion()) { - LOGGER.debug("Update default chart '" + chart.getName() + "' from version " + currentChart.getVersion() + " to " + chart.getVersion()); + LOGGER.debug(MessageFormat.format("Update default chart ''{0}'' from version {1} to {2}", chart.getName(), currentChart.getVersion(), chart.getVersion())); currentChart.setVersion(chart.getVersion()); currentChart.setScript(chart.getScript()); chartRepository.save(currentChart); @@ -91,7 +93,7 @@ private void updateUserCharts() String script = userChart.getScript(); if(script.contains(PATTERN_OLD_CONTAINER_ID)) { - LOGGER.debug("Updating user chart '" + userChart.getName() + "' with ID " + userChart.getID()); + LOGGER.debug(MessageFormat.format("Updating user chart ''{0}'' with ID {1}", userChart.getName(), userChart.getID())); script = script.replace(PATTERN_OLD_CONTAINER_ID, PATTERN_DYNAMIC_CONTAINER_ID); userChart.setScript(script); getRepository().save(userChart); diff --git a/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java b/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java index 8aaee465f..b27732158 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java +++ b/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.net.URL; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; @@ -72,7 +73,7 @@ private static String getChartFromFile(String filePath) URL url = DefaultCharts.class.getClassLoader().getResource(filePath); if(url == null) { - LOGGER.warn("Couldn't add default chart '" + filePath + "' due to missing file"); + LOGGER.warn(MessageFormat.format("Couldn''t add default chart ''{0}'' due to missing file", filePath)); return ""; } diff --git a/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java b/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java index b4f6c695c..de1ed1204 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/controller/AboutController.java @@ -1,13 +1,21 @@ package de.deadlocker8.budgetmaster.controller; import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; + @Controller +@RequestMapping(Mappings.ABOUT) public class AboutController extends BaseController { private final SettingsService settingsService; @@ -18,10 +26,32 @@ public AboutController(SettingsService settingsService) this.settingsService = settingsService; } - @RequestMapping("/about") + @GetMapping public String index(Model model) { model.addAttribute("settings", settingsService.getSettings()); return "about"; } + + @GetMapping("/whatsNewModal") + public String whatsNewModal(Model model) + { + final List newsEntries = new ArrayList<>(); + newsEntries.add(NewsEntry.createWithLocalizationKeys("news.changeType.headline", "news.changeType.description")); + newsEntries.add(NewsEntry.createWithLocalizationKeys("news.readonlyAccounts.headline", "news.readonlyAccounts.description")); + newsEntries.add(NewsEntry.createWithLocalizationKeys("news.firstUseWizard.headline", "news.firstUseWizard.description")); + newsEntries.add(NewsEntry.createWithLocalizationKeys("news.java11.headline", "news.java11.description")); + + model.addAttribute("newsEntries", newsEntries); + return "whatsNewModal"; + } + + @RequestMapping("/whatsNewModal/close") + @Transactional + public String whatsNewModalClose(HttpServletRequest request, Model model) + { + settingsService.getSettings().setWhatsNewShownForCurrentVersion(true); + model.addAttribute("settings", settingsService.getSettings()); + return "redirect:" + request.getHeader("Referer"); + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/controller/BackupController.java b/src/main/java/de/deadlocker8/budgetmaster/controller/BackupController.java index 234c2913e..843f116ec 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/controller/BackupController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/controller/BackupController.java @@ -1,6 +1,7 @@ package de.deadlocker8.budgetmaster.controller; import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -10,6 +11,7 @@ @Controller +@RequestMapping(Mappings.BACKUP_REMINDER) public class BackupController extends BaseController { private final SettingsService settingsService; @@ -20,7 +22,7 @@ public BackupController(SettingsService settingsService) this.settingsService = settingsService; } - @RequestMapping("/backupReminder/cancel") + @RequestMapping("/cancel") public String cancel(HttpServletRequest request, Model model) { settingsService.updateLastBackupReminderDate(); @@ -28,7 +30,7 @@ public String cancel(HttpServletRequest request, Model model) return "redirect:" + request.getHeader("Referer"); } - @RequestMapping("/backupReminder/settings") + @RequestMapping("/settings") public String settings() { settingsService.updateLastBackupReminderDate(); diff --git a/src/main/java/de/deadlocker8/budgetmaster/controller/HotKeysController.java b/src/main/java/de/deadlocker8/budgetmaster/controller/HotKeysController.java index 95ae9cd60..38ad6198c 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/controller/HotKeysController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/controller/HotKeysController.java @@ -1,6 +1,7 @@ package de.deadlocker8.budgetmaster.controller; import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -18,7 +19,7 @@ public HotKeysController(SettingsService settingsService) this.settingsService = settingsService; } - @RequestMapping("/hotkeys") + @RequestMapping(Mappings.HOTKEYS) public String index(Model model) { model.addAttribute("settings", settingsService.getSettings()); diff --git a/src/main/java/de/deadlocker8/budgetmaster/controller/IndexController.java b/src/main/java/de/deadlocker8/budgetmaster/controller/IndexController.java index 8b1423812..b2fe115a9 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/controller/IndexController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/controller/IndexController.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -18,10 +19,17 @@ public IndexController(SettingsService settingsService) this.settingsService = settingsService; } - @RequestMapping("/") + @RequestMapping public String index(Model model) { model.addAttribute("settings", settingsService.getSettings()); return "index"; } + + @GetMapping("/firstUse") + public String firstUse(Model model) + { + model.addAttribute("settings", settingsService.getSettings()); + return "firstUse"; + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/controller/NewsEntry.java b/src/main/java/de/deadlocker8/budgetmaster/controller/NewsEntry.java new file mode 100644 index 000000000..c7ed16289 --- /dev/null +++ b/src/main/java/de/deadlocker8/budgetmaster/controller/NewsEntry.java @@ -0,0 +1,39 @@ +package de.deadlocker8.budgetmaster.controller; + +import de.thecodelabs.utils.util.Localization; + +public class NewsEntry +{ + private String headline; + private String description; + + public NewsEntry(String headline, String description) + { + this.headline = headline; + this.description = description; + } + + public static NewsEntry createWithLocalizationKeys(String headlineKey, String descriptionKey) + { + return new NewsEntry(Localization.getString(headlineKey), Localization.getString(descriptionKey)); + } + + public String getHeadline() + { + return headline; + } + + public String getDescription() + { + return description; + } + + @Override + public String toString() + { + return "NewsEntry{" + + "headline='" + headline + '\'' + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/src/main/java/de/deadlocker8/budgetmaster/controller/TeapotController.java b/src/main/java/de/deadlocker8/budgetmaster/controller/TeapotController.java index d5fc0e947..a273dd5e9 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/controller/TeapotController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/controller/TeapotController.java @@ -1,5 +1,6 @@ package de.deadlocker8.budgetmaster.controller; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -7,7 +8,7 @@ @Controller public class TeapotController extends BaseController { - @RequestMapping("/418") + @RequestMapping(Mappings.TEAPOT) public String index() { return "error/418"; diff --git a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser.java b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser.java index e725f1f65..4094442a6 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser.java +++ b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser.java @@ -8,6 +8,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.MessageFormat; + public class DatabaseParser { final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); @@ -32,26 +34,26 @@ public Database parseDatabaseFromJSON() throws IllegalArgumentException } int version = root.get("VERSION").getAsInt(); - LOGGER.info("Parsing BudgetMaster database with version " + version); + LOGGER.info(MessageFormat.format("Parsing BudgetMaster database with version {0}", version)); if(version == 2) { final Database database = new LegacyParser(jsonString, categoryNone).parseDatabaseFromJSON(); - LOGGER.debug("Parsed database with " + database.getTransactions().size() + " transactions, " + database.getCategories().size() + " categories and " + database.getAccounts().size() + " accounts"); + LOGGER.debug(MessageFormat.format("Parsed database with {0} transactions, {1} categories and {2} accounts", database.getTransactions().size(), database.getCategories().size(), database.getAccounts().size())); return database; } if(version == 3) { final Database database = new DatabaseParser_v3(jsonString).parseDatabaseFromJSON(); - LOGGER.debug("Parsed database with " + database.getTransactions().size() + " transactions, " + database.getCategories().size() + " categories and " + database.getAccounts().size() + " accounts"); + LOGGER.debug(MessageFormat.format("Parsed database with {0} transactions, {1} categories and {2} accounts", database.getTransactions().size(), database.getCategories().size(), database.getAccounts().size())); return database; } if(version == 4) { final Database database = new DatabaseParser_v4(jsonString).parseDatabaseFromJSON(); - LOGGER.debug("Parsed database with " + database.getTransactions().size() + " transactions, " + database.getCategories().size() + " categories and " + database.getAccounts().size() + " accounts and " + database.getTemplates().size() + " templates"); + LOGGER.debug(MessageFormat.format("Parsed database with {0} transactions, {1} categories, {2} accounts and {3} templates", database.getTransactions().size(), database.getCategories().size(), database.getAccounts().size(), database.getTemplates().size())); return database; } diff --git a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v3.java b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v3.java index dd52b1789..2e0a64fc1 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v3.java +++ b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v3.java @@ -105,14 +105,14 @@ protected List parseTransactions(JsonObject root) return parsedTransactions; } - private RepeatingOption parseRepeatingOption(JsonObject transactiob, DateTime startDate) + protected RepeatingOption parseRepeatingOption(JsonObject transaction, DateTime startDate) { - if(!transactiob.has("repeatingOption")) + if(!transaction.has("repeatingOption")) { return null; } - JsonObject option = transactiob.get("repeatingOption").getAsJsonObject(); + JsonObject option = transaction.get("repeatingOption").getAsJsonObject(); JsonObject repeatingModifier = option.get("modifier").getAsJsonObject(); String repeatingModifierType = repeatingModifier.get("localizationKey").getAsString(); diff --git a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v4.java b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v4.java index eeba1be6f..a80ebbec7 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v4.java +++ b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseParser_v4.java @@ -6,6 +6,9 @@ import com.google.gson.JsonParser; import de.deadlocker8.budgetmaster.templates.Template; import de.deadlocker8.budgetmaster.transactions.Transaction; +import de.deadlocker8.budgetmaster.transactions.TransactionBase; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +39,53 @@ public Database parseDatabaseFromJSON() throws IllegalArgumentException return new Database(categories, accounts, transactions, templates); } + @Override + protected List parseTransactions(JsonObject root) + { + List parsedTransactions = new ArrayList<>(); + JsonArray transactions = root.get("transactions").getAsJsonArray(); + for(JsonElement currentTransaction : transactions) + { + final JsonObject transactionObject = currentTransaction.getAsJsonObject(); + + + int amount = transactionObject.get("amount").getAsInt(); + String name = transactionObject.get("name").getAsString(); + String description = transactionObject.get("description").getAsString(); + + Transaction transaction = new Transaction(); + transaction.setAmount(amount); + transaction.setName(name); + transaction.setDescription(description); + transaction.setTags(parseTags(transactionObject)); + + int categoryID = transactionObject.get("category").getAsJsonObject().get("ID").getAsInt(); + transaction.setCategory(getCategoryByID(categoryID)); + + int accountID = transactionObject.get("account").getAsJsonObject().get("ID").getAsInt(); + transaction.setAccount(getAccountByID(accountID)); + + JsonElement transferAccount = transactionObject.get("transferAccount"); + if(transferAccount != null) + { + int transferAccountID = transferAccount.getAsJsonObject().get("ID").getAsInt(); + transaction.setTransferAccount(getAccountByID(transferAccountID)); + } + + String date = transactionObject.get("date").getAsString(); + DateTime parsedDate = DateTime.parse(date, DateTimeFormat.forPattern("yyyy-MM-dd")); + transaction.setDate(parsedDate); + + transaction.setRepeatingOption(super.parseRepeatingOption(transactionObject, parsedDate)); + + handleIsExpenditure(transactionObject, transaction); + + parsedTransactions.add(transaction); + } + + return parsedTransactions; + } + protected List parseTemplates(JsonObject root) { final List parsedTemplates = new ArrayList<>(); @@ -77,6 +127,8 @@ protected List parseTemplates(JsonObject root) final Optional transferAccountOptional = parseIDOfElementIfExists(templateObject, "transferAccount"); transferAccountOptional.ifPresent(integer -> template.setTransferAccount(super.getAccountByID(integer))); + handleIsExpenditure(templateObject, template); + parsedTemplates.add(template); } @@ -90,6 +142,27 @@ private Optional parseIDOfElementIfExists(JsonObject jsonObject, String { return Optional.of(element.getAsJsonObject().get("ID").getAsInt()); } - return Optional.empty(); + return Optional.empty(); + } + + private void handleIsExpenditure(JsonObject jsonObject, TransactionBase transactionBase) + { + final JsonElement isExpenditure = jsonObject.get("isExpenditure"); + if(isExpenditure == null) + { + if(transactionBase.getAmount() == null) + { + transactionBase.setIsExpenditure(true); + } + else + { + transactionBase.setIsExpenditure(transactionBase.getAmount() <= 0); + } + } + else + { + transactionBase.setIsExpenditure(isExpenditure.getAsBoolean()); + } } + } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java index 9a824a2b8..e14439f3d 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/database/DatabaseService.java @@ -30,6 +30,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -143,17 +144,17 @@ public List determineFilesToDelete(Path backupFolderPath) final List existingBackups = getExistingBackups(backupFolderPath); if(existingBackups.size() < numberOfFilesToKeep) { - LOGGER.debug("Skipping backup rotation (existing backups: " + existingBackups.size() + ", files to keep: " + numberOfFilesToKeep + ")"); + LOGGER.debug(MessageFormat.format("Skipping backup rotation (existing backups: {0}, files to keep: {1})", existingBackups.size(), numberOfFilesToKeep)); return filesToDelete; } - LOGGER.debug("Determining old backups (existing backups: " + existingBackups.size() + ", files to keep: " + numberOfFilesToKeep + ")"); + LOGGER.debug(MessageFormat.format("Determining old backups (existing backups: {0}, files to keep: {1})", existingBackups.size(), numberOfFilesToKeep)); // reserve 1 file for the backup created afterwards final int allowedNumberOfFiles = existingBackups.size() - numberOfFilesToKeep + 1; for(int i = 0; i < allowedNumberOfFiles; i++) { final Path oldBackup = Paths.get(existingBackups.get(i)); - LOGGER.debug("Schedule old backup for deletion: " + oldBackup.toString()); + LOGGER.debug(MessageFormat.format("Schedule old backup for deletion: {0}", oldBackup.toString())); filesToDelete.add(oldBackup); } @@ -215,7 +216,7 @@ public static String getExportFileName(boolean includeTime) public Database getDatabaseForJsonSerialization() { - List categories = categoryService.getRepository().findAll(); + List categories = categoryService.getAllCategories(); List accounts = accountService.getRepository().findAll(); List transactions = transactionService.getRepository().findAll(); List filteredTransactions = filterRepeatingTransactions(transactions); diff --git a/src/main/java/de/deadlocker8/budgetmaster/filter/FilterController.java b/src/main/java/de/deadlocker8/budgetmaster/filter/FilterController.java index 111f84d27..88fcb674c 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/filter/FilterController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/filter/FilterController.java @@ -1,6 +1,7 @@ package de.deadlocker8.budgetmaster.filter; import de.deadlocker8.budgetmaster.controller.BaseController; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -11,6 +12,7 @@ @Controller +@RequestMapping(Mappings.FILTER) public class FilterController extends BaseController { private final FilterHelpersService filterHelpers; @@ -21,14 +23,14 @@ public FilterController(FilterHelpersService filterHelpers) this.filterHelpers = filterHelpers; } - @PostMapping(value = "/filter/apply") + @PostMapping(value = "/apply") public String post(WebRequest request, @ModelAttribute("NewFilterConfiguration") FilterConfiguration filterConfiguration) { request.setAttribute("filterConfiguration", filterConfiguration, WebRequest.SCOPE_SESSION); return "redirect:" + request.getHeader("Referer"); } - @GetMapping("/filter/reset") + @GetMapping("/reset") public String reset(WebRequest request) { FilterConfiguration filterConfiguration = FilterConfiguration.DEFAULT; diff --git a/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndAfterXTimes.java b/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndAfterXTimes.java index 50f72e93a..4dede91d3 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndAfterXTimes.java +++ b/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndAfterXTimes.java @@ -5,6 +5,7 @@ import javax.persistence.*; import java.util.List; +import java.util.Objects; @Entity public class RepeatingEndAfterXTimes extends RepeatingEnd @@ -32,4 +33,20 @@ public Object getValue() { return times; } + + @Override + public boolean equals(Object o) + { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + if(!super.equals(o)) return false; + RepeatingEndAfterXTimes that = (RepeatingEndAfterXTimes) o; + return times == that.times; + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), times); + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndDate.java b/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndDate.java index 3ac3a3194..a7dc605d5 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndDate.java +++ b/src/main/java/de/deadlocker8/budgetmaster/repeating/endoption/RepeatingEndDate.java @@ -6,6 +6,7 @@ import javax.persistence.*; import java.util.List; +import java.util.Objects; @Entity public class RepeatingEndDate extends RepeatingEnd @@ -35,4 +36,20 @@ public Object getValue() { return endDate; } + + @Override + public boolean equals(Object o) + { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + if(!super.equals(o)) return false; + RepeatingEndDate that = (RepeatingEndDate) o; + return Objects.equals(endDate, that.endDate); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), endDate); + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/reports/Fonts.java b/src/main/java/de/deadlocker8/budgetmaster/reports/Fonts.java index 81c7a9c02..c56e5adf8 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/reports/Fonts.java +++ b/src/main/java/de/deadlocker8/budgetmaster/reports/Fonts.java @@ -2,5 +2,9 @@ public class Fonts { + private Fonts() + { + } + public static final String OPEN_SANS = "fonts/OpenSans-Regular.ttf"; } diff --git a/src/main/java/de/deadlocker8/budgetmaster/reports/ReportController.java b/src/main/java/de/deadlocker8/budgetmaster/reports/ReportController.java index 4479e516a..1645e87c9 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/reports/ReportController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/reports/ReportController.java @@ -15,6 +15,7 @@ import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.transactions.TransactionService; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.thecodelabs.utils.util.Localization; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; @@ -29,10 +30,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.text.MessageFormat; import java.util.List; @Controller +@RequestMapping(Mappings.REPORTS) public class ReportController extends BaseController { private final SettingsService settingsService; @@ -57,7 +60,7 @@ public ReportController(SettingsService settingsService, ReportSettingsService r this.filterHelpers = filterHelpers; } - @RequestMapping("/reports") + @RequestMapping public String reports(HttpServletRequest request, Model model, @CookieValue(value = "currentDate", required = false) String cookieDate) { DateTime date = dateService.getDateTimeFromCookie(cookieDate); @@ -69,7 +72,7 @@ public String reports(HttpServletRequest request, Model model, @CookieValue(valu return "reports/reports"; } - @PostMapping(value = "/reports/generate") + @PostMapping(value = "/generate") public void post(HttpServletRequest request, HttpServletResponse response, @ModelAttribute("NewReportSettings") ReportSettings reportSettings) { @@ -94,13 +97,13 @@ public void post(HttpServletRequest request, HttpServletResponse response, .setReportSettings(reportSettings) .setTransactions(transactions) .setAccountName(accountName) - .setCategoryBudgets(CategoryBudgetHandler.getCategoryBudgets(transactions, categoryService.getRepository().findAll())) + .setCategoryBudgets(CategoryBudgetHandler.getCategoryBudgets(transactions, categoryService.getAllCategories())) .createReportConfiguration(); String month = reportSettings.getDate().toString("MM"); String year = reportSettings.getDate().toString("YYYY"); - LOGGER.debug("Exporting month report (month: " + year + "_" + month + ", account: " + accountName + ")..."); + LOGGER.debug(MessageFormat.format("Exporting month report (month: {0}_{1}, account: {2})...", year, month, accountName)); //generate PDF try diff --git a/src/main/java/de/deadlocker8/budgetmaster/reports/ReportGeneratorService.java b/src/main/java/de/deadlocker8/budgetmaster/reports/ReportGeneratorService.java index 1000f39f4..db5320d1b 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/reports/ReportGeneratorService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/reports/ReportGeneratorService.java @@ -7,6 +7,7 @@ import de.deadlocker8.budgetmaster.reports.columns.ReportColumn; import de.deadlocker8.budgetmaster.services.CurrencyService; import de.deadlocker8.budgetmaster.services.DateFormatStyle; +import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.tags.Tag; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.utils.Strings; @@ -17,27 +18,29 @@ import java.io.ByteArrayOutputStream; import java.util.List; +import java.util.Locale; import java.util.stream.Collectors; @Service public class ReportGeneratorService { - @Autowired - CurrencyService currencyService; - - private final String FONT = Fonts.OPEN_SANS; + private static final String FONT = Fonts.OPEN_SANS; + private final CurrencyService currencyService; + private final SettingsService settingsService; @Autowired - public ReportGeneratorService(CurrencyService currencyService) + public ReportGeneratorService(CurrencyService currencyService, SettingsService settingsService) { this.currencyService = currencyService; + this.settingsService = settingsService; } private Chapter generateHeader(ReportConfiguration reportConfiguration) { Font font = FontFactory.getFont(FONT, BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 16, Font.BOLDITALIC, BaseColor.BLACK); - Chunk chunk = new Chunk(Localization.getString(Strings.REPORT_HEADLINE, reportConfiguration.getReportSettings().getDate().toString("MMMM yyyy")), font); + Locale locale = settingsService.getSettings().getLanguage().getLocale(); + Chunk chunk = new Chunk(Localization.getString(Strings.REPORT_HEADLINE, reportConfiguration.getReportSettings().getDate().toString("MMMM yyyy", locale)), font); Chapter chapter = new Chapter(new Paragraph(chunk), 1); chapter.setNumberDepth(0); @@ -211,10 +214,7 @@ public byte[] generate(ReportConfiguration reportConfiguration) throws DocumentE document.add(Chunk.NEWLINE); PdfPTable table = generateCategoryBudgets(reportConfiguration); - if(table != null) - { - document.add(table); - } + document.add(table); } document.close(); diff --git a/src/main/java/de/deadlocker8/budgetmaster/reports/categoryBudget/CategoryBudgetHandler.java b/src/main/java/de/deadlocker8/budgetmaster/reports/categoryBudget/CategoryBudgetHandler.java index c8877ccee..081d047f0 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/reports/categoryBudget/CategoryBudgetHandler.java +++ b/src/main/java/de/deadlocker8/budgetmaster/reports/categoryBudget/CategoryBudgetHandler.java @@ -7,6 +7,10 @@ public class CategoryBudgetHandler { + private CategoryBudgetHandler() + { + } + public static List getCategoryBudgets(List transactions, List categories) { List budgets = new ArrayList<>(); diff --git a/src/main/java/de/deadlocker8/budgetmaster/reports/columns/ReportColumnService.java b/src/main/java/de/deadlocker8/budgetmaster/reports/columns/ReportColumnService.java index 62a3d43bd..f49e826bf 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/reports/columns/ReportColumnService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/reports/columns/ReportColumnService.java @@ -10,8 +10,9 @@ @Service public class ReportColumnService { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); - private ReportColumnRepository reportColumnRepository; + private static final Logger LOGGER = LoggerFactory.getLogger(ReportColumnService.class); + + private final ReportColumnRepository reportColumnRepository; @Autowired public ReportColumnService(ReportColumnRepository reportColumnRepository) diff --git a/src/main/java/de/deadlocker8/budgetmaster/reports/settings/ReportSettingsService.java b/src/main/java/de/deadlocker8/budgetmaster/reports/settings/ReportSettingsService.java index 903ac1b73..8d6356736 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/reports/settings/ReportSettingsService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/reports/settings/ReportSettingsService.java @@ -13,10 +13,10 @@ @Service public class ReportSettingsService { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(ReportSettingsService.class); - private ReportSettingsRepository reportSettingsRepository; - private ReportColumnService reportColumnService; + private final ReportSettingsRepository reportSettingsRepository; + private final ReportColumnService reportColumnService; @Autowired public ReportSettingsService(ReportSettingsRepository reportSettingsRepository, ReportColumnService reportColumnService) diff --git a/src/main/java/de/deadlocker8/budgetmaster/search/SearchController.java b/src/main/java/de/deadlocker8/budgetmaster/search/SearchController.java index 4082dbca4..bb88ab1c4 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/search/SearchController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/search/SearchController.java @@ -5,6 +5,7 @@ import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.transactions.TransactionSearchSpecifications; import de.deadlocker8.budgetmaster.transactions.TransactionService; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -12,8 +13,6 @@ 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.bind.annotation.RequestMapping; @Controller @@ -29,7 +28,7 @@ public SearchController(TransactionService transactionService, SettingsService s this.settingsService = settingsService; } - @GetMapping(value = "/search") + @GetMapping(Mappings.SEARCH) public String search(Model model, Search search) { if(search.isEmptySearch()) diff --git a/src/main/java/de/deadlocker8/budgetmaster/services/ErrorCodeController.java b/src/main/java/de/deadlocker8/budgetmaster/services/ErrorCodeController.java index 55fc1240a..bdd296d24 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/services/ErrorCodeController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/services/ErrorCodeController.java @@ -1,5 +1,6 @@ package de.deadlocker8.budgetmaster.services; +import de.deadlocker8.budgetmaster.utils.Mappings; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; @@ -14,10 +15,10 @@ public class ErrorCodeController implements ErrorController @Override public String getErrorPath() { - return "/error"; + return Mappings.ERROR; } - @RequestMapping("/error") + @RequestMapping(Mappings.ERROR) public String handleError(HttpServletRequest request) { final Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); diff --git a/src/main/java/de/deadlocker8/budgetmaster/services/HelpersService.java b/src/main/java/de/deadlocker8/budgetmaster/services/HelpersService.java index 50bf4d93b..c5b10116a 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/services/HelpersService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/services/HelpersService.java @@ -22,6 +22,7 @@ import de.thecodelabs.utils.util.ColorUtilsNonJavaFX; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -49,6 +50,9 @@ public class HelpersService @Autowired private CategoryRepository categoryRepository; + @Value("${budgetmaster.datepicker.simple:false}") + private boolean useSimpleDatepickerForTransactions; + public List getAvailableLanguages() { return Arrays.asList(LanguageType.values()); @@ -195,4 +199,9 @@ public Long getUsageCountForCategory(Category category) { return transactionService.getRepository().countByCategory(category); } + + public boolean isUseSimpleDatepickerForTransactions() + { + return useSimpleDatepickerForTransactions; + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java b/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java index e7b34f8e3..6546cf697 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java @@ -19,13 +19,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; @Service public class ImportService { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(ImportService.class); private final CategoryRepository categoryRepository; private final TransactionRepository transactionRepository; @@ -62,13 +63,13 @@ public Database getDatabase() private void importCategories() { List categories = database.getCategories(); - LOGGER.debug("Importing " + categories.size() + " categories..."); + LOGGER.debug(MessageFormat.format("Importing {0} categories...", categories.size())); List alreadyUpdatedTransactions = new ArrayList<>(); List alreadyUpdatedTemplates = new ArrayList<>(); for(Category category : categories) { - LOGGER.debug("Importing category " + category.getName()); + LOGGER.debug(MessageFormat.format("Importing category {0}", category.getName())); Category existingCategory; if(category.getType().equals(CategoryType.NONE) || category.getType().equals(CategoryType.REST)) { @@ -137,14 +138,14 @@ public List updateCategoriesForItems(List item private void importAccounts(AccountMatchList accountMatchList) { - LOGGER.debug("Importing " + accountMatchList.getAccountMatches().size() + " accounts..."); + LOGGER.debug(MessageFormat.format("Importing {0} accounts...", accountMatchList.getAccountMatches().size())); List alreadyUpdatedTransactions = new ArrayList<>(); List alreadyUpdatedTransferTransactions = new ArrayList<>(); List alreadyUpdatedTemplates = new ArrayList<>(); for(AccountMatch accountMatch : accountMatchList.getAccountMatches()) { - LOGGER.debug("Importing account " + accountMatch.getAccountSource().getName() + " -> " + accountMatch.getAccountDestination().getName()); + LOGGER.debug(MessageFormat.format("Importing account {0} -> {1}", accountMatch.getAccountSource().getName(), accountMatch.getAccountDestination().getName())); List transactions = new ArrayList<>(database.getTransactions()); transactions.removeAll(alreadyUpdatedTransactions); @@ -155,7 +156,7 @@ private void importAccounts(AccountMatchList accountMatchList) alreadyUpdatedTransferTransactions.addAll(updateTransferAccountsForTransactions(transferTransactions, accountMatch.getAccountSource().getID(), accountMatch.getAccountDestination())); List templates = new ArrayList<>(database.getTemplates()); - transactions.removeAll(alreadyUpdatedTemplates); + templates.removeAll(alreadyUpdatedTemplates); alreadyUpdatedTemplates.addAll(updateAccountsForItems(templates, accountMatch.getAccountSource().getID(), accountMatch.getAccountDestination())); } @@ -211,11 +212,11 @@ public List updateTransferAccountsForTransactions(List transactions = database.getTransactions(); - LOGGER.debug("Importing " + transactions.size() + " transactions..."); + LOGGER.debug(MessageFormat.format("Importing {0} transactions...", transactions.size())); for(int i = 0; i < transactions.size(); i++) { Transaction transaction = transactions.get(i); - LOGGER.debug("Importing transaction " + (i + 1) + "/" + transactions.size() + " (name: " + transaction.getName() + ", date: " + transaction.getDate() + ")"); + LOGGER.debug(MessageFormat.format("Importing transaction {0}/{1} (name: {2}, date: {3})", i + 1, transactions.size(), transaction.getName(), transaction.getDate())); updateTagsForItem(transaction); transaction.setID(null); transactionRepository.save(transaction); @@ -245,11 +246,11 @@ public void updateTagsForItem(TransactionBase item) private void importTemplates() { List templates = database.getTemplates(); - LOGGER.debug("Importing " + templates.size() + " templates..."); + LOGGER.debug(MessageFormat.format("Importing {0} templates...", templates.size())); for(int i = 0; i < templates.size(); i++) { Template template = templates.get(i); - LOGGER.debug("Importing template " + (i + 1) + "/" + templates.size() + " (templateName: " + template.getTemplateName() + ")"); + LOGGER.debug(MessageFormat.format("Importing template {0}/{1} (templateName: {2})", i + 1, templates.size(), template.getTemplateName())); updateTagsForItem(template); template.setID(null); templateRepository.save(template); diff --git a/src/main/java/de/deadlocker8/budgetmaster/services/LocalizationService.java b/src/main/java/de/deadlocker8/budgetmaster/services/LocalizationService.java index 4b2c3326f..b17f89124 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/services/LocalizationService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/services/LocalizationService.java @@ -30,9 +30,9 @@ public Locale getLocale() } @Override - public String getBaseResource() + public String[] getBaseResources() { - return "languages/"; + return new String[]{"languages/base", "languages/news"}; } @Override @@ -40,4 +40,10 @@ public LocalizationMessageFormatter messageFormatter() { return new JavaMessageFormatter(); } + + @Override + public boolean useMultipleResourceBundles() + { + return true; + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java b/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java index 7d03c617c..cc8edcb12 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java +++ b/src/main/java/de/deadlocker8/budgetmaster/settings/Settings.java @@ -29,6 +29,8 @@ public class Settings private AutoBackupTime autoBackupTime; private Integer autoBackupFilesToKeep; private Integer installedVersionCode; + private Boolean whatsNewShownForCurrentVersion; + private Boolean showFirstUseBanner; public Settings() { @@ -50,6 +52,8 @@ public static Settings getDefault() defaultSettings.setAutoBackupTime(AutoBackupTime.TIME_00); defaultSettings.setAutoBackupFilesToKeep(3); defaultSettings.setInstalledVersionCode(0); + defaultSettings.setWhatsNewShownForCurrentVersion(false); + defaultSettings.setShowFirstUseBanner(true); return defaultSettings; } @@ -198,6 +202,31 @@ public void setInstalledVersionCode(Integer installedVersionCode) this.installedVersionCode = installedVersionCode; } + public Boolean getWhatsNewShownForCurrentVersion() + { + return whatsNewShownForCurrentVersion; + } + + public void setWhatsNewShownForCurrentVersion(Boolean whatsNewShownForCurrentVersion) + { + this.whatsNewShownForCurrentVersion = whatsNewShownForCurrentVersion; + } + + public boolean needToShowWhatsNew() + { + return !this.whatsNewShownForCurrentVersion; + } + + public Boolean getShowFirstUseBanner() + { + return showFirstUseBanner; + } + + public void setShowFirstUseBanner(Boolean showFirstUseBanner) + { + this.showFirstUseBanner = showFirstUseBanner; + } + @Override public String toString() { @@ -216,6 +245,8 @@ public String toString() ", autoBackupTime=" + autoBackupTime + ", autoBackupFilesToKeep=" + autoBackupFilesToKeep + ", installedVersionCode=" + installedVersionCode + + ", whatsNewShownForCurrentVersion=" + whatsNewShownForCurrentVersion + + ", showFirstUseBanner=" + showFirstUseBanner + '}'; } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java index b66606749..8080f5179 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java @@ -11,17 +11,16 @@ import de.deadlocker8.budgetmaster.database.DatabaseParser; import de.deadlocker8.budgetmaster.database.DatabaseService; import de.deadlocker8.budgetmaster.database.accountmatches.AccountMatchList; -import de.deadlocker8.budgetmaster.services.ImportService; import de.deadlocker8.budgetmaster.services.BackupService; +import de.deadlocker8.budgetmaster.services.ImportService; import de.deadlocker8.budgetmaster.update.BudgetMasterUpdateService; import de.deadlocker8.budgetmaster.utils.LanguageType; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.Strings; import de.thecodelabs.utils.util.Localization; import de.thecodelabs.utils.util.RandomUtils; import de.thecodelabs.versionizer.UpdateItem; import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Controller; @@ -38,12 +37,14 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; import java.util.Arrays; import java.util.List; import java.util.Optional; @Controller +@RequestMapping(Mappings.SETTINGS) public class SettingsController extends BaseController { private final SettingsService settingsService; @@ -55,7 +56,6 @@ public class SettingsController extends BaseController private final BudgetMasterUpdateService budgetMasterUpdateService; private final BackupService scheduleTaskService; - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); private final List SEARCH_RESULTS_PER_PAGE_OPTIONS = Arrays.asList(10, 20, 25, 30, 50, 100); @Autowired @@ -71,7 +71,7 @@ public SettingsController(SettingsService settingsService, UserRepository userRe this.scheduleTaskService = scheduleTaskService; } - @GetMapping("/settings") + @GetMapping public String settings(WebRequest request, Model model) { model.addAttribute("settings", settingsService.getSettings()); @@ -85,7 +85,7 @@ public String settings(WebRequest request, Model model) return "settings/settings"; } - @PostMapping(value = "/settings/save") + @PostMapping(value = "/save") public String post(Model model, @ModelAttribute("Settings") Settings settings, BindingResult bindingResult, @RequestParam(value = "password") String password, @RequestParam(value = "passwordConfirmation") String passwordConfirmation, @@ -176,7 +176,7 @@ else if(password.length() < 3) return Optional.empty(); } - @GetMapping("/settings/database/requestExport") + @GetMapping("/database/requestExport") public void downloadFile(HttpServletResponse response) { LOGGER.debug("Exporting database..."); @@ -204,7 +204,7 @@ public void downloadFile(HttpServletResponse response) } } - @GetMapping("/settings/database/requestDelete") + @GetMapping("/database/requestDelete") public String requestDeleteDatabase(Model model) { String verificationCode = RandomUtils.generateRandomString(RandomUtils.RandomType.BASE_58, 4, RandomUtils.RandomStringPolicy.UPPER, RandomUtils.RandomStringPolicy.DIGIT); @@ -216,7 +216,7 @@ public String requestDeleteDatabase(Model model) return "settings/settings"; } - @PostMapping(value = "/settings/database/delete") + @PostMapping(value = "/database/delete") public String deleteDatabase(Model model, @RequestParam("verificationCode") String verificationCode, @RequestParam("verificationUserInput") String verificationUserInput) { @@ -237,7 +237,7 @@ public String deleteDatabase(Model model, @RequestParam("verificationCode") Stri return "settings/settings"; } - @GetMapping("/settings/database/requestImport") + @GetMapping("/database/requestImport") public String requestImportDatabase(Model model) { model.addAttribute("importDatabase", true); @@ -247,7 +247,7 @@ public String requestImportDatabase(Model model) return "settings/settings"; } - @RequestMapping("/settings/database/upload") + @RequestMapping("/database/upload") public String upload(WebRequest request, Model model, @RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { if(file.isEmpty()) @@ -258,7 +258,7 @@ public String upload(WebRequest request, Model model, @RequestParam("file") Mult try { String jsonString = new String(file.getBytes(), StandardCharsets.UTF_8); - DatabaseParser importer = new DatabaseParser(jsonString, categoryService.getRepository().findByType(CategoryType.NONE)); + DatabaseParser importer = new DatabaseParser(jsonString, categoryService.findByType(CategoryType.NONE)); Database database = importer.parseDatabaseFromJSON(); request.setAttribute("database", database, WebRequest.SCOPE_SESSION); @@ -276,16 +276,16 @@ public String upload(WebRequest request, Model model, @RequestParam("file") Mult } } - @GetMapping("/settings/database/accountMatcher") + @GetMapping("/database/accountMatcher") public String openAccountMatcher(WebRequest request, Model model) { model.addAttribute("database", request.getAttribute("database", WebRequest.SCOPE_SESSION)); - model.addAttribute("availableAccounts", accountService.getAllAccountsAsc()); + model.addAttribute("availableAccounts", accountService.getAllActivatedAccountsAsc()); model.addAttribute("settings", settingsService.getSettings()); return "settings/import"; } - @PostMapping("/settings/database/import") + @PostMapping("/database/import") public String importDatabase(WebRequest request, @ModelAttribute("Import") AccountMatchList accountMatchList, Model model) { importService.importDatabase((Database) request.getAttribute("database", WebRequest.SCOPE_SESSION), accountMatchList); @@ -333,9 +333,16 @@ public String performUpdate() e.printStackTrace(); } - LOGGER.info("Stopping BudgetMaster for update to version " + budgetMasterUpdateService.getAvailableVersionString()); + LOGGER.info(MessageFormat.format("Stopping BudgetMaster for update to version {0}", budgetMasterUpdateService.getAvailableVersionString())); System.exit(0); return ""; } + + @RequestMapping("/hideFirstUseBanner") + public String hideFirstUseBanner() + { + settingsService.disableFirstUseBanner(); + return "redirect:/"; + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java index 27ea656ec..1bc4e724c 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsService.java @@ -14,7 +14,7 @@ @Service public class SettingsService { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(SettingsService.class); private final SettingsRepository settingsRepository; @Autowired @@ -35,7 +35,7 @@ public void postInit() @Transactional public void createDefaultSettingsIfNotExists() { - if(!settingsRepository.findById(0).isPresent()) + if(settingsRepository.findById(0).isEmpty()) { settingsRepository.save(Settings.getDefault()); LOGGER.debug("Created default settings"); @@ -43,7 +43,7 @@ public void createDefaultSettingsIfNotExists() Settings defaultSettings = Settings.getDefault(); Optional settingsOptional = settingsRepository.findById(0); - if(!settingsOptional.isPresent()) + if(settingsOptional.isEmpty()) { throw new RuntimeException("Missing Settings in database"); } @@ -81,6 +81,14 @@ public void createDefaultSettingsIfNotExists() { settings.setInstalledVersionCode(defaultSettings.getInstalledVersionCode()); } + if(settings.getWhatsNewShownForCurrentVersion() == null) + { + settings.setWhatsNewShownForCurrentVersion(defaultSettings.getWhatsNewShownForCurrentVersion()); + } + if(settings.getShowFirstUseBanner() == null) + { + settings.setShowFirstUseBanner(defaultSettings.getShowFirstUseBanner()); + } } @SuppressWarnings("OptionalGetWithoutIsPresent") @@ -96,6 +104,13 @@ public void updateLastBackupReminderDate() settings.setLastBackupReminderDate(DateTime.now()); } + @Transactional + public void disableFirstUseBanner() + { + Settings settings = getSettings(); + settings.setShowFirstUseBanner(false); + } + @Transactional public void updateSettings(Settings newSettings) { diff --git a/src/main/java/de/deadlocker8/budgetmaster/tags/TagScheduler.java b/src/main/java/de/deadlocker8/budgetmaster/tags/TagScheduler.java index 18ba74553..5cc67b22e 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/tags/TagScheduler.java +++ b/src/main/java/de/deadlocker8/budgetmaster/tags/TagScheduler.java @@ -14,7 +14,7 @@ @Service public class TagScheduler { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(TagScheduler.class); private final TagRepository tagRepository; private final TransactionRepository transactionRepository; diff --git a/src/main/java/de/deadlocker8/budgetmaster/tags/TagService.java b/src/main/java/de/deadlocker8/budgetmaster/tags/TagService.java index 46cd542b5..d1b8de81c 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/tags/TagService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/tags/TagService.java @@ -1,15 +1,12 @@ package de.deadlocker8.budgetmaster.tags; import de.deadlocker8.budgetmaster.services.Resetable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class TagService implements Resetable { - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); private TagRepository tagRepository; @Autowired diff --git a/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateController.java b/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateController.java index 1f7a484fb..57e710b7a 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateController.java @@ -8,6 +8,7 @@ import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.transactions.TransactionService; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.ResourceNotFoundException; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; @@ -18,12 +19,11 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Controller +@RequestMapping(Mappings.TEMPLATES) public class TemplateController extends BaseController { private static final Gson GSON = new GsonBuilder() @@ -46,7 +46,7 @@ public TemplateController(TemplateService templateService, SettingsService setti this.accountService = accountService; } - @GetMapping("/templates") + @GetMapping public String showTemplates(Model model) { model.addAttribute("settings", settingsService.getSettings()); @@ -54,22 +54,14 @@ public String showTemplates(Model model) return "templates/templates"; } - @GetMapping("/templates/select") - public String select(Model model) - { - model.addAttribute("settings", settingsService.getSettings()); - model.addAttribute("templates", templateService.getRepository().findAllByOrderByTemplateNameAsc()); - return "templates/selectTemplate"; - } - - @GetMapping("/templates/fromTransactionModal") + @GetMapping("/fromTransactionModal") public String fromTransactionModal(Model model) { model.addAttribute("existingTemplateNames", GSON.toJson(templateService.getExistingTemplateNames())); return "templates/createFromTransactionModal"; } - @PostMapping(value = "/templates/fromTransaction") + @PostMapping(value = "/fromTransaction") public String postFromTransaction(@RequestParam(value = "templateName") String templateName, @ModelAttribute("NewTransaction") Transaction transaction, @RequestParam(value = "includeCategory") Boolean includeCategory, @@ -90,11 +82,11 @@ public String postFromTransaction(@RequestParam(value = "templateName") String t return "redirect:/templates"; } - @GetMapping("/templates/{ID}/requestDelete") + @GetMapping("/{ID}/requestDelete") public String requestDeleteTemplate(Model model, @PathVariable("ID") Integer ID) { final Optional templateOptional = templateService.getRepository().findById(ID); - if(!templateOptional.isPresent()) + if(templateOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -105,35 +97,44 @@ public String requestDeleteTemplate(Model model, @PathVariable("ID") Integer ID) return "templates/templates"; } - @GetMapping("/templates/{ID}/delete") + @GetMapping("/{ID}/delete") public String deleteTemplate(@PathVariable("ID") Integer ID) { templateService.getRepository().deleteById(ID); return "redirect:/templates"; } - @GetMapping("/templates/{ID}/select") + @GetMapping("/{ID}/select") public String selectTemplate(Model model, @CookieValue("currentDate") String cookieDate, @PathVariable("ID") Integer ID) { final Optional templateOptional = templateService.getRepository().findById(ID); - if(!templateOptional.isPresent()) + if(templateOptional.isEmpty()) { throw new ResourceNotFoundException(); } final Template template = templateOptional.get(); - - templateService.prepareTemplateForNewTransaction(template, true); - - if(template.getAmount() == null) + final Transaction newTransaction = new Transaction(); + newTransaction.setName(template.getName()); + newTransaction.setAmount(template.getAmount()); + newTransaction.setCategory(template.getCategory()); + newTransaction.setDescription(template.getDescription()); + newTransaction.setAccount(template.getAccount()); + newTransaction.setTransferAccount(template.getTransferAccount()); + newTransaction.setTags(template.getTags()); + newTransaction.setIsExpenditure(template.isExpenditure()); + + templateService.prepareTemplateForNewTransaction(newTransaction, true); + + if(newTransaction.getAmount() == null && newTransaction.isExpenditure() == null) { template.setIsExpenditure(true); } final DateTime date = dateService.getDateTimeFromCookie(cookieDate); - transactionService.prepareModelNewOrEdit(model, false, date, template, accountService.getAllAccountsAsc()); + transactionService.prepareModelNewOrEdit(model, false, date, null, template, accountService.getAllActivatedAccountsAsc()); if(template.isTransfer()) { @@ -142,16 +143,16 @@ public String selectTemplate(Model model, return "transactions/newTransactionNormal"; } - @GetMapping("/templates/newTemplate") + @GetMapping("/newTemplate") public String newTemplate(Model model) { final Template emptyTemplate = new Template(); templateService.prepareTemplateForNewTransaction(emptyTemplate, false); - templateService.prepareModelNewOrEdit(model, false, emptyTemplate, accountService.getAllAccountsAsc()); + templateService.prepareModelNewOrEdit(model, false, emptyTemplate, accountService.getAllActivatedAccountsAsc()); return "templates/newTemplate"; } - @PostMapping(value = "/templates/newTemplate") + @PostMapping(value = "/newTemplate") public String post(Model model, @ModelAttribute("NewTemplate") Template template, BindingResult bindingResult, @RequestParam(value = "includeAccount", required = false) boolean includeAccount, @@ -175,7 +176,7 @@ public String post(Model model, if(template.isExpenditure() == null) { - template.setIsExpenditure(true); + template.setIsExpenditure(false); } if(template.getAmount() != null) @@ -187,7 +188,7 @@ public String post(Model model, if(bindingResult.hasErrors()) { model.addAttribute("error", bindingResult); - templateService.prepareModelNewOrEdit(model, template.getID() != null, template, accountService.getAllAccountsAsc()); + templateService.prepareModelNewOrEdit(model, template.getID() != null, template, accountService.getAllActivatedAccountsAsc()); return "templates/newTemplate"; } @@ -205,18 +206,18 @@ public String post(Model model, return "redirect:/templates"; } - @GetMapping("/templates/{ID}/edit") + @GetMapping("/{ID}/edit") public String editTemplate(Model model, @PathVariable("ID") Integer ID) { Optional templateOptional = templateService.getRepository().findById(ID); - if(!templateOptional.isPresent()) + if(templateOptional.isEmpty()) { throw new ResourceNotFoundException(); } Template template = templateOptional.get(); templateService.prepareTemplateForNewTransaction(template, false); - templateService.prepareModelNewOrEdit(model, true, template, accountService.getAllAccountsAsc()); + templateService.prepareModelNewOrEdit(model, true, template, accountService.getAllActivatedAccountsAsc()); return "templates/newTemplate"; } diff --git a/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateService.java b/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateService.java index b11fd292d..3c41b17d9 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/templates/TemplateService.java @@ -10,8 +10,6 @@ import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.transactions.TransactionBase; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.ui.Model; @@ -27,7 +25,6 @@ public class TemplateService implements Resetable .setPrettyPrinting() .create(); - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); private final TemplateRepository templateRepository; private final AccountService accountService; private final CategoryService categoryService; @@ -75,11 +72,11 @@ public void createFromTransaction(String templateName, Transaction transaction, getRepository().save(template); } - public void prepareTemplateForNewTransaction(Template template, boolean prepareAccount) + public void prepareTemplateForNewTransaction(TransactionBase template, boolean prepareAccount) { if(template.getCategory() == null) { - template.setCategory(categoryService.getRepository().findByType(CategoryType.NONE)); + template.setCategory(categoryService.findByType(CategoryType.NONE)); } if(prepareAccount && template.getAccount() == null) @@ -87,6 +84,12 @@ public void prepareTemplateForNewTransaction(Template template, boolean prepareA final Account selectedAccount = accountService.getRepository().findByIsSelected(true); template.setAccount(selectedAccount); } + + final Account account = template.getAccount(); + if(account != null && account.isReadOnly()) + { + template.setAccount(accountService.getRepository().findByIsDefault(true)); + } } public void prepareModelNewOrEdit(Model model, boolean isEdit, TransactionBase item, List accounts) diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionBase.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionBase.java index 5589d577f..30b07d66d 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionBase.java +++ b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionBase.java @@ -18,6 +18,8 @@ public interface TransactionBase Category getCategory(); + void setCategory(Category category); + List getTags(); void setTags(List tags); diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionController.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionController.java index acb0b578b..20dcc22b4 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionController.java +++ b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionController.java @@ -16,6 +16,7 @@ import de.deadlocker8.budgetmaster.services.DateService; import de.deadlocker8.budgetmaster.services.HelpersService; import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.ResourceNotFoundException; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; @@ -27,10 +28,12 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; +import java.text.MessageFormat; import java.util.List; import java.util.Optional; @Controller +@RequestMapping(Mappings.TRANSACTIONS) public class TransactionController extends BaseController { private final TransactionService transactionService; @@ -55,7 +58,7 @@ public TransactionController(TransactionService transactionService, CategoryServ this.filterHelpers = filterHelpers; } - @GetMapping("/transactions") + @GetMapping public String transactions(HttpServletRequest request, Model model, @CookieValue(value = "currentDate", required = false) String cookieDate) { DateTime date = dateService.getDateTimeFromCookie(cookieDate); @@ -66,7 +69,7 @@ public String transactions(HttpServletRequest request, Model model, @CookieValue return "transactions/transactions"; } - @GetMapping("/transactions/{ID}/requestDelete") + @GetMapping("/{ID}/requestDelete") public String requestDeleteTransaction(HttpServletRequest request, Model model, @PathVariable("ID") Integer ID, @CookieValue("currentDate") String cookieDate) { if(!transactionService.isDeletable(ID)) @@ -94,30 +97,33 @@ private void prepareModelTransactions(FilterConfiguration filterConfiguration, M model.addAttribute("settings", settingsService.getSettings()); } - @GetMapping("/transactions/{ID}/delete") + @GetMapping("/{ID}/delete") public String deleteTransaction(@PathVariable("ID") Integer ID) { transactionService.deleteTransaction(ID); return "redirect:/transactions"; } - @GetMapping("/transactions/newTransaction/{type}") + @GetMapping("/newTransaction/{type}") public String newTransaction(Model model, @CookieValue("currentDate") String cookieDate, @PathVariable String type) { DateTime date = dateService.getDateTimeFromCookie(cookieDate); Transaction emptyTransaction = new Transaction(); - emptyTransaction.setCategory(categoryService.getRepository().findByType(CategoryType.NONE)); - transactionService.prepareModelNewOrEdit(model, false, date, emptyTransaction, accountService.getAllAccountsAsc()); + emptyTransaction.setCategory(categoryService.findByType(CategoryType.NONE)); + transactionService.prepareModelNewOrEdit(model, false, date, null, emptyTransaction, accountService.getAllActivatedAccountsAsc()); return "transactions/newTransaction" + StringUtils.capitalize(type); } - @PostMapping(value = "/transactions/newTransaction/normal") + @PostMapping(value = "/newTransaction/normal") public String postNormal(Model model, @CookieValue("currentDate") String cookieDate, - @ModelAttribute("NewTransaction") Transaction transaction, BindingResult bindingResult) + @ModelAttribute("NewTransaction") Transaction transaction, BindingResult bindingResult, + @RequestParam(value = "previousType", required = false) TransactionType previousType) { DateTime date = dateService.getDateTimeFromCookie(cookieDate); + handlePreviousType(previousType, transaction); + TransactionValidator transactionValidator = new TransactionValidator(); transactionValidator.validate(transaction, bindingResult); @@ -129,11 +135,20 @@ public String postNormal(Model model, return handleRedirect(model, transaction.getID() != null, transaction, bindingResult, date, "transactions/newTransactionNormal"); } + private void handlePreviousType(TransactionType previousType, Transaction transaction) + { + if(previousType == TransactionType.REPEATING) + { + transactionService.deleteTransaction(transaction.getID()); + } + } + @SuppressWarnings("ConstantConditions") - @PostMapping(value = "/transactions/newTransaction/repeating") + @PostMapping(value = "/newTransaction/repeating") public String postRepeating(Model model, @CookieValue("currentDate") String cookieDate, @ModelAttribute("NewTransaction") Transaction transaction, BindingResult bindingResult, @RequestParam(value = "isRepeating", required = false) boolean isRepeating, + @RequestParam(value = "previousType", required = false) TransactionType previousType, @RequestParam(value = "repeatingModifierNumber", required = false) int repeatingModifierNumber, @RequestParam(value = "repeatingModifierType", required = false) String repeatingModifierType, @RequestParam(value = "repeatingEndType", required = false) String repeatingEndType, @@ -179,13 +194,16 @@ public String postRepeating(Model model, @CookieValue("currentDate") String cook return handleRedirect(model, transaction.getID() != null, transaction, bindingResult, date, "transactions/newTransactionRepeating"); } - @PostMapping(value = "/transactions/newTransaction/transfer") + @PostMapping(value = "/newTransaction/transfer") public String postTransfer(Model model, @CookieValue("currentDate") String cookieDate, - @ModelAttribute("NewTransaction") Transaction transaction, BindingResult bindingResult) + @ModelAttribute("NewTransaction") Transaction transaction, BindingResult bindingResult, + @RequestParam(value = "previousType", required = false) TransactionType previousType) { DateTime date = dateService.getDateTimeFromCookie(cookieDate); + handlePreviousType(previousType, transaction); + TransactionValidator transactionValidator = new TransactionValidator(); transactionValidator.validate(transaction, bindingResult); @@ -202,7 +220,7 @@ private String handleRedirect(Model model, boolean isEdit, @ModelAttribute("NewT if(bindingResult.hasErrors()) { model.addAttribute("error", bindingResult); - transactionService.prepareModelNewOrEdit(model, isEdit, date, transaction, accountService.getAllAccountsAsc()); + transactionService.prepareModelNewOrEdit(model, isEdit, date, null, transaction, accountService.getAllActivatedAccountsAsc()); return url; } @@ -210,17 +228,22 @@ private String handleRedirect(Model model, boolean isEdit, @ModelAttribute("NewT return "redirect:/transactions"; } - @GetMapping("/transactions/{ID}/edit") + @GetMapping("/{ID}/edit") public String editTransaction(Model model, @CookieValue("currentDate") String cookieDate, @PathVariable("ID") Integer ID) { Optional transactionOptional = transactionService.getRepository().findById(ID); - if(!transactionOptional.isPresent()) + if(transactionOptional.isEmpty()) { throw new ResourceNotFoundException(); } Transaction transaction = transactionOptional.get(); + if(transaction.getAccount().isReadOnly()) + { + return "redirect:/transactions"; + } + // select first transaction in order to provide correct start date for repeating transactions if(transaction.getRepeatingOption() != null) { @@ -228,7 +251,7 @@ public String editTransaction(Model model, @CookieValue("currentDate") String co } DateTime date = dateService.getDateTimeFromCookie(cookieDate); - transactionService.prepareModelNewOrEdit(model, true, date, transaction, accountService.getAllAccountsAsc()); + transactionService.prepareModelNewOrEdit(model, true, date, null, transaction, accountService.getAllActivatedAccountsAsc()); if(transaction.isRepeating()) { @@ -242,7 +265,7 @@ public String editTransaction(Model model, @CookieValue("currentDate") String co return "transactions/newTransactionNormal"; } - @GetMapping("/transactions/{ID}/highlight") + @GetMapping("/{ID}/highlight") public String highlight(Model model, @PathVariable("ID") Integer ID) { Transaction transaction = transactionService.getRepository().getOne(ID); @@ -257,4 +280,71 @@ public String highlight(Model model, @PathVariable("ID") Integer ID) model.addAttribute("highlightID", ID); return "transactions/transactions"; } + + @GetMapping("/{ID}/changeTypeModal") + public String changeTypeModal(Model model, @PathVariable("ID") Integer ID) + { + final Optional transactionOptional = transactionService.getRepository().findById(ID); + if(transactionOptional.isEmpty()) + { + throw new ResourceNotFoundException(); + } + + model.addAttribute("transaction", transactionOptional.get()); + return "transactions/changeTypeModal"; + } + + @GetMapping("/{ID}/changeType") + public String changeTypeModal(Model model, @PathVariable("ID") Integer ID, + @CookieValue("currentDate") String cookieDate, + @RequestParam(value = "newType") int newType) + { + final Optional transactionOptional = transactionService.getRepository().findById(ID); + if(transactionOptional.isEmpty()) + { + throw new ResourceNotFoundException(); + } + + final Optional transactionTypeOptional = TransactionType.getByID(newType); + if(transactionTypeOptional.isEmpty()) + { + throw new IllegalArgumentException(); + } + + Transaction transaction = transactionOptional.get(); + // select first transaction in order to provide correct start date for repeating transactions + if(transaction.getRepeatingOption() != null) + { + transaction = transaction.getRepeatingOption().getReferringTransactions().get(0); + } + + Transaction transactionCopy = new Transaction(transaction); + final TransactionType newTransactionType = transactionTypeOptional.get(); + LOGGER.debug(MessageFormat.format("Changing transaction type to {0} for transaction with ID {1}", newTransactionType, String.valueOf(transaction.getID()))); + + final TransactionType previousType = TransactionType.getFromTransaction(transaction); + + String redirectUrl = ""; + switch(newTransactionType) + { + case NORMAL: + transactionCopy.setTransferAccount(null); + transactionCopy.setRepeatingOption(null); + redirectUrl = "transactions/newTransactionNormal"; + break; + case REPEATING: + transactionCopy.setTransferAccount(null); + redirectUrl = "transactions/newTransactionRepeating"; + break; + case TRANSFER: + transactionCopy.setRepeatingOption(null); + redirectUrl = "transactions/newTransactionTransfer"; + break; + } + + DateTime date = dateService.getDateTimeFromCookie(cookieDate); + transactionService.prepareModelNewOrEdit(model, true, date, previousType, transactionCopy, accountService.getAllActivatedAccountsAsc()); + + return redirectUrl; + } } \ No newline at end of file diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionService.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionService.java index 969d49b94..52d401fbc 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionService.java @@ -4,7 +4,6 @@ import com.google.gson.GsonBuilder; import de.deadlocker8.budgetmaster.accounts.Account; import de.deadlocker8.budgetmaster.accounts.AccountType; -import de.deadlocker8.budgetmaster.categories.CategoryRepository; import de.deadlocker8.budgetmaster.categories.CategoryService; import de.deadlocker8.budgetmaster.categories.CategoryType; import de.deadlocker8.budgetmaster.filter.FilterConfiguration; @@ -26,6 +25,7 @@ import org.springframework.stereotype.Service; import org.springframework.ui.Model; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -39,13 +39,13 @@ public class TransactionService implements Resetable .setPrettyPrinting() .create(); - private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + private static final Logger LOGGER = LoggerFactory.getLogger(TransactionService.class); - private TransactionRepository transactionRepository; - private RepeatingOptionRepository repeatingOptionRepository; - private CategoryService categoryService; - private TagService tagService; - private SettingsService settingsService; + private final TransactionRepository transactionRepository; + private final RepeatingOptionRepository repeatingOptionRepository; + private final CategoryService categoryService; + private final TagService tagService; + private final SettingsService settingsService; @Autowired public TransactionService(TransactionRepository transactionRepository, RepeatingOptionRepository repeatingOptionRepository, CategoryService categoryService, TagService tagService, SettingsService settingsService) @@ -83,7 +83,7 @@ private List getTransactionsForMonthAndYearWithRest(Account account List transactions = getTransactionsForMonthAndYearWithoutRest(account, month, year, filterConfiguration); Transaction transactionRest = new Transaction(); - transactionRest.setCategory(categoryService.getRepository().findByType(CategoryType.REST)); + transactionRest.setCategory(categoryService.findByType(CategoryType.REST)); transactionRest.setName(Localization.getString(Strings.CATEGORY_REST)); transactionRest.setDate(DateTime.now().withYear(year).withMonthOfYear(month).withDayOfMonth(1)); transactionRest.setAmount(getRest(account, startDate)); @@ -162,9 +162,9 @@ public void deleteTransaction(Integer ID) private void deleteTransactionInRepo(Integer ID) { Optional transactionOptional = transactionRepository.findById(ID); - if(!transactionOptional.isPresent()) + if(transactionOptional.isEmpty()) { - LOGGER.debug("Skipping already deleted transaction with ID: " + ID); + LOGGER.debug(MessageFormat.format("Skipping already deleted transaction with ID: {0}", ID)); return; } Transaction transactionToDelete = transactionOptional.get(); @@ -188,7 +188,12 @@ public boolean isDeletable(Integer ID) final Transaction transaction = transactionOptional.get(); if(transaction.getCategory() != null) { - return transaction.getCategory().getType() != CategoryType.REST; + if(transaction.getCategory().getType() == CategoryType.REST) + { + return false; + } + + return !transaction.getAccount().isReadOnly(); } } return false; @@ -284,10 +289,11 @@ private TransactionBase addTagForTransactionBase(String name, TransactionBase it return item; } - public void prepareModelNewOrEdit(Model model, boolean isEdit, DateTime date, TransactionBase item, List accounts) + public void prepareModelNewOrEdit(Model model, boolean isEdit, DateTime date, TransactionType previousType, TransactionBase item, List accounts) { model.addAttribute("isEdit", isEdit); model.addAttribute("currentDate", date); + model.addAttribute("previousType", previousType); model.addAttribute("categories", categoryService.getAllCategories()); model.addAttribute("accounts", accounts); model.addAttribute("transaction", item); diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionSpecifications.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionSpecifications.java index 61a44cb69..325747e71 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionSpecifications.java +++ b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionSpecifications.java @@ -13,6 +13,10 @@ public class TransactionSpecifications { + private TransactionSpecifications() + { + } + public static Specification withDynamicQuery(final DateTime startDate, final DateTime endDate, Account account, final boolean isIncome, boolean isExpenditure, boolean isTransfer, diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionType.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionType.java new file mode 100644 index 000000000..2bae53042 --- /dev/null +++ b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionType.java @@ -0,0 +1,61 @@ +package de.deadlocker8.budgetmaster.transactions; + +import java.util.Optional; + +public enum TransactionType +{ + NORMAL(1), + REPEATING(2), + TRANSFER(3); + + private int typeID; + + TransactionType(int typeID) + { + this.typeID = typeID; + } + + public int getTypeID() + { + return typeID; + } + + public static Optional getByID(int typeID) + { + switch(typeID) + { + case 1: + return Optional.of(NORMAL); + case 2: + return Optional.of(REPEATING); + case 3: + return Optional.of(TRANSFER); + default: + return Optional.empty(); + } + } + + public static TransactionType getFromTransaction(Transaction transaction) + { + if(transaction.isTransfer()) + { + return TRANSFER; + } + else if(transaction.isRepeating()) + { + return REPEATING; + } + else + { + return NORMAL; + } + } + + @Override + public String toString() + { + return "TransactionType{" + + "typeID=" + typeID + + '}'; + } +} diff --git a/src/main/java/de/deadlocker8/budgetmaster/update/BudgetMasterUpdateService.java b/src/main/java/de/deadlocker8/budgetmaster/update/BudgetMasterUpdateService.java index 39023c02f..a7d397b70 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/update/BudgetMasterUpdateService.java +++ b/src/main/java/de/deadlocker8/budgetmaster/update/BudgetMasterUpdateService.java @@ -23,6 +23,7 @@ import java.io.File; import java.nio.file.Paths; +import java.text.MessageFormat; @Service public class BudgetMasterUpdateService @@ -79,7 +80,7 @@ public void updateSearchTask() { UpdateAvailableEvent customSpringEvent = new UpdateAvailableEvent(this, updateService); applicationEventPublisher.publishEvent(customSpringEvent); - LOGGER.info("Update available (installed: v" + Build.getInstance().getVersionName() + ", available: " + getAvailableVersionString() + ")"); + LOGGER.info(MessageFormat.format("Update available (installed: v{0}, available: {1})", Build.getInstance().getVersionName(), getAvailableVersionString())); } } } diff --git a/src/main/java/de/deadlocker8/budgetmaster/utils/Mappings.java b/src/main/java/de/deadlocker8/budgetmaster/utils/Mappings.java new file mode 100644 index 000000000..ad3c5dd04 --- /dev/null +++ b/src/main/java/de/deadlocker8/budgetmaster/utils/Mappings.java @@ -0,0 +1,24 @@ +package de.deadlocker8.budgetmaster.utils; + +public final class Mappings +{ + private Mappings() + { + } + + public static final String ABOUT = "/about"; + public static final String ACCOUNTS = "/accounts"; + public static final String BACKUP_REMINDER = "/backupReminder"; + public static final String CATEGORIES = "/categories"; + public static final String CHARTS = "/charts"; + public static final String ERROR = "/error"; + public static final String FILTER = "/filter"; + public static final String HOTKEYS = "/hotkeys"; + public static final String LOGIN = "/login"; + public static final String REPORTS = "/reports"; + public static final String SEARCH = "/search"; + public static final String SETTINGS = "/settings"; + public static final String TEAPOT = "/418"; + public static final String TEMPLATES = "/templates"; + public static final String TRANSACTIONS = "/transactions"; +} diff --git a/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java b/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java index eeca9389e..6b875bd1f 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java +++ b/src/main/java/de/deadlocker8/budgetmaster/utils/eventlistener/UpdateInstalledVersion.java @@ -1,6 +1,7 @@ package de.deadlocker8.budgetmaster.utils.eventlistener; import de.deadlocker8.budgetmaster.Build; +import de.deadlocker8.budgetmaster.settings.Settings; import de.deadlocker8.budgetmaster.settings.SettingsService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +35,14 @@ public void onApplicationEvent(ApplicationStartedEvent event) final Build build = Build.getInstance(); final int runningVersionCode = Integer.parseInt(build.getVersionCode()); + final Settings settings = settingsService.getSettings(); + if(settings.getInstalledVersionCode() < runningVersionCode) + { + LOGGER.debug("Reset 'whatsNewShownForCurrentVersion'"); + settings.setWhatsNewShownForCurrentVersion(false); + } + LOGGER.debug(MessageFormat.format("Updated installedVersionCode to {0}", runningVersionCode)); - settingsService.getSettings().setInstalledVersionCode(runningVersionCode); + settings.setInstalledVersionCode(runningVersionCode); } } \ No newline at end of file diff --git a/src/main/resources/languages/_de.properties b/src/main/resources/languages/base_de.properties similarity index 84% rename from src/main/resources/languages/_de.properties rename to src/main/resources/languages/base_de.properties index 86714465b..229d7bb78 100644 --- a/src/main/resources/languages/_de.properties +++ b/src/main/resources/languages/base_de.properties @@ -11,7 +11,7 @@ errorpages.home=Zur Startseite errorpages.400=Ungültige Anfrage. errorpages.403=Zugriff nicht gestattet. errorpages.404=Die angegebene Seite konnte nicht gefunden werden. -errorpages.418=I'm a teapot. +errorpages.418=I''m a teapot. errorpages.418.credits=Teapot icon made by Freepik from www.flaticon.com is licensed by CC 3.0 BY errorpages.500=Ein interner Serverfehler ist aufgetreten. @@ -67,6 +67,9 @@ placeholder.seems.empty=Ganz schön leer hier... placeholder.advice=Füge {0} hinzu save.as.template=Vorlage erzeugen save.as.template.errorsInForm=Vorlage konnte nicht erstellt werden, da Fehler im Formular existieren! +transaction.change.type=Buchungstyp ändern +transaction.change.type.warning=Hinweis: Nicht gespeicherte Änderungen gehen verloren! +transaction.change.type.new=Neuer Buchungstyp # WEEK DAYS monday=Montag @@ -140,6 +143,8 @@ warning.settings.password.confirmation.wrong=Passwort und Passwort Wiederholung warning.empty.chart.name=Bitte gib einen Namen ein. warning.empty.chart.script=Bitte gib ein Script ein. warning.duplicate.template.name=Es existiert bereits eine Vorlage mit diesem Namen. +warning.transaction.date=Das angegebene Datum entspricht nicht dem erlaubten Format. Erwartetes Format: DD.MM.YY, DDMMYY, DD.MM.YYYY, DDMMYYYY. + # UI menu.home=Startseite @@ -152,6 +157,7 @@ menu.settings=Einstellungen menu.settings.database=Datenbank menu.about=Über menu.hotkeys=Tastenkombination +menu.firstUseGuide=Einführung menu.logout=Logout menu.accounts=Konten menu.update=Update verfügbar @@ -204,6 +210,8 @@ account.default.name=Standardkonto account.all=Alle Konten account.budget.asof=Stand account.tooltip.default=Als Standardkonto festlegen +account.tooltip.readonly.activate=Konto aktivieren +account.tooltip.readonly.deactivate=Konto deaktivieren transaction.new.label.name=Name transaction.new.label.amount=Betrag @@ -256,6 +264,7 @@ template.checkbox.include.account.transfer=Zielkonto übernehmen about=Über {0} about.roadmap.link=Roadmap öffnen about.version=Version: +about.version.whatsnew=Was gibt es neues about.date=Datum: about.author=Autor: about.roadmap=Roadmap: @@ -318,6 +327,9 @@ filter.tags.button.all=Alle filter.tags.button.none=Keine # home menu +home.first.use.teaser=Neu im BudgetMaster? Sieh dir die Einführung an! +home.first.use=Tipps für die erste Benutzung + home.menu.accounts=Konten erlauben es mehrere Buchungen zu gruppieren. Du kannst so viele Konten erstellen, wie du möchtest. home.menu.accounts.action.manage=Kontoverwaltung home.menu.accounts.action.new=Neues Konto anlegen @@ -343,6 +355,36 @@ home.menu.categories.action.new=Neue Kategorie anlegen home.menu.settings=Verwalte allgemeine Einstellungen wie dein Login-Passwort, deine bevorzugte Sprache und wie Updates verwaltet werden sollen. Dieser Bereich bietet zudem die Möglichkeit, deine Daten zu exportieren oder zu löschen, sowie eine bestehende Datenbank zu importieren. home.menu.settings.action.manage=Einstellungen +home.first.use.step.1.headline=Schritt 1: Konten erstellen +home.first.use.step.1.contentText=BudgetMaster erstellt beim ersten Start automatisch ein Standardkonto.Um BudgetMaster besser an deine Bedürfnissen anzupassen, kannst das Konto umbenennen oder zusätzliche Konten anlegen. + +home.first.use.step.2.headline=Schritt 2: Kategorien erstellen +home.first.use.step.2.contentText=Kategorien können Buchungen zugeordnet werden, um diese als zusammengehörig zu kennzeichnen.Erstelle einige Kategorien, um sie später verwenden zu können. + +home.first.use.step.3.headline=Schritt 3: Deinen aktuellen Kontostand übertragen +home.first.use.step.3.contentText=In den meisten Fällen wirst du BudgetMaster erst verwenden, nachdem dein Bankkonto angelegt wurde und bereits einige Buchungen getätigt wurden.Um deinen aktuellen Kontostand zu übertragen, lege eine neue normale Buchung an: +home.first.use.step.3.sub.1=Markiere die Transaktion als Einnahme oben auf der Seite. +home.first.use.step.3.sub.2=Gib einen Namen ein, z.B. "Initialer Kontostand". +home.first.use.step.3.sub.3=Lege den Betrag auf deinen aktuellen Kontostand fest. +home.first.use.step.3.sub.4=Überlege, welches das erste Datum ist, dass du in BudgetMaster verwalten möchtest. Setze das Datum der Buchung auf einen Wert vor diesem Datum. +home.first.use.step.3.sub.5=Wähle das gewünschte Konto. +home.first.use.step.3.sub.6=Speichere die Buchung. + +home.first.use.step.4.headline=Schritt 4: Buchungen erstellen +home.first.use.step.4.contentText=Buchungen werden in drei Kategorien gruppiert: +home.first.use.step.4.sub.1=Normale Buchungen - Einfache Buchungen (Einnahmen/Ausgaben) +home.first.use.step.4.sub.2=Wiederholende Buchungen - Widerholen sich automatisch in einem bestimmten Intervall +home.first.use.step.4.sub.3=Umbuchungen - Beträge zwischen Konten übertragen + +home.first.use.step.5.headline=Schritt 5: Erkunden! +home.first.use.step.5.contentText=Nachdem du nun die Grundlagen von BudgetMaster kennengelernt hast entdecke auch die restlichen Funktionen: +home.first.use.step.5.sub.1=Beschleunige die Erstellung von Buchungen. +home.first.use.step.5.sub.2=Verwende eines der vordefinierten Diagramme oder erstelle dein eigenes, indem du das Diagramm-framework zur Visualisierung und Analyse deiner Daten verwendest. +home.first.use.step.5.sub.3=Erstelle konfigurierbare Monatsberichte im PDF-Format zum Drucken und Archivieren. +home.first.use.step.5.sub.4=und vieles mehr... + +home.first.use.home=Los geht's! + # hotkeys hotkeys.transactions.new.normal=Neue Buchung anlegen hotkeys.transactions.new.normal.key=n @@ -352,6 +394,9 @@ hotkeys.transactions.new.transfer=Neue Umbuchung anlegen hotkeys.transactions.new.transfer.key=t hotkeys.transactions.new.template=Neue Buchung aus Vorlage anlegen hotkeys.transactions.new.template.key=v +hotkeys.transactions.save.modifier=Strg +hotkeys.transactions.save.key=Enter +hotkeys.transactions.save=Buchung speichern (Beim Anlegen/Editieren einer Buchung) hotkeys.transactions.filter=Filtern hotkeys.transactions.filter.key=f hotkeys.search=Suchen diff --git a/src/main/resources/languages/_en.properties b/src/main/resources/languages/base_en.properties similarity index 85% rename from src/main/resources/languages/_en.properties rename to src/main/resources/languages/base_en.properties index a96346853..c8eb808b3 100644 --- a/src/main/resources/languages/_en.properties +++ b/src/main/resources/languages/base_en.properties @@ -67,6 +67,9 @@ placeholder.seems.empty=It''s pretty empty here... placeholder.advice=Get started by adding {0} save.as.template=Create template save.as.template.errorsInForm=Template could not be created because errors exist in the form! +transaction.change.type=Change type +transaction.change.type.warning=Note: Unsaved changes will be lost! +transaction.change.type.new=New type # WEEK DAYS monday=Monday @@ -140,6 +143,7 @@ warning.settings.password.confirmation.wrong=Password and password confirmation warning.empty.chart.name=Please insert a name. warning.empty.chart.script=Please insert a script. warning.duplicate.template.name=A template with this name is already existing. +warning.transaction.date=The specified date does not correspond to the allowed format. Expected format: DD.MM.YY, DDMMYY, DD.MM.YYYY, DDMMYYYY. # UI menu.home=Home @@ -152,6 +156,7 @@ menu.settings=Settings menu.settings.database=Database menu.hotkeys=Hotkeys menu.about=About +menu.firstUseGuide=Introduction menu.logout=Logout menu.accounts=Accounts menu.update=Update available @@ -204,6 +209,8 @@ account.default.name=Default Account account.all=All Accounts account.budget.asof=as of account.tooltip.default=Set as default account +account.tooltip.readonly.activate=Enable account +account.tooltip.readonly.deactivate=Disable account transaction.new.label.name=Name transaction.new.label.amount=Amount @@ -256,6 +263,7 @@ template.checkbox.include.account.transfer=Include destination account about=About {0} about.roadmap.link=Open Roadmap about.version=Version: +about.version.whatsnew=What''s new about.date=Date: about.author=Author: about.roadmap=Roadmap: @@ -318,6 +326,9 @@ filter.tags.button.all=All filter.tags.button.none=None # home menu +home.first.use.teaser=New to BudgetMaster? Check out the first use guide! +home.first.use=First use guide + home.menu.accounts=Accounts allow you to group multiple transactions. You can create as many accounts as you want. home.menu.accounts.action.manage=Manage accounts home.menu.accounts.action.new=Create an account @@ -343,6 +354,36 @@ home.menu.categories.action.new=Create a category home.menu.settings=Manage general settings such as login password, your preferred language and how to handle updates. This section also offers the possibility to export or delete your data or importing an existing database. home.menu.settings.action.manage=Settings +home.first.use.step.1.headline=Step 1: Create accounts +home.first.use.step.1.contentText=BudgetMaster will automatically create a default account on first start.In order to fit your needs you may want to rename it or create additional accounts. + +home.first.use.step.2.headline=Step 2: Create categories +home.first.use.step.2.contentText=Categories can be assigned to transactions in order to mark them as belonging together.Create some categories to be used later. + +home.first.use.step.3.headline=Step 3: Insert your current account balance +home.first.use.step.3.contentText=In most cases you will start using BudgetMaster after your bank account was created and there are already some transactions made.To transfer your current account balance create a new normal transaction: +home.first.use.step.3.sub.1=Mark the transaction as income on the top of the page. +home.first.use.step.3.sub.2=Type a name, e.g. "start account balance" +home.first.use.step.3.sub.3=Set the amount to your current account balance. +home.first.use.step.3.sub.4=Decide which is the first date you want to track in BudgetMaster. Set the transaction date to a value before this date. +home.first.use.step.3.sub.5=Select the desired account. +home.first.use.step.3.sub.6=Save the transaction. + +home.first.use.step.4.headline=Step 4: Create transactions +home.first.use.step.4.contentText=Transactions are grouped into three categories: +home.first.use.step.4.sub.1=Normal transactions - Basic transactions (incomes/expenditures) +home.first.use.step.4.sub.2=Recurring transactions - Automatically repeat on a given interval +home.first.use.step.4.sub.3=Transfer transactions - Transfer amounts between accounts + +home.first.use.step.5.headline=Step 5: Explore! +home.first.use.step.5.contentText=Now that you now the fundamentals of BudgetMaster, go and discover the remaining features: +home.first.use.step.5.sub.1=Speed up your transaction creation process. +home.first.use.step.5.sub.2=Use one of the pre-defined charts or create your one by using the chart framework to visualize and analyze your data. +home.first.use.step.5.sub.3=Create configurable month reports in PDF format for printing and archiving. +home.first.use.step.5.sub.4=and much more... + +home.first.use.home=Let''s go! + # hotkeys hotkeys.transactions.new.normal=Create a transaction hotkeys.transactions.new.normal.key=n @@ -352,6 +393,9 @@ hotkeys.transactions.new.transfer=Create a transfer hotkeys.transactions.new.transfer.key=t hotkeys.transactions.new.template=Create a transaction from template hotkeys.transactions.new.template.key=v +hotkeys.transactions.save.modifier=Ctrl +hotkeys.transactions.save.key=Enter +hotkeys.transactions.save=Save transaction (When creating/editing a transaction) hotkeys.transactions.filter=Filter hotkeys.transactions.filter.key=f hotkeys.search=Search diff --git a/src/main/resources/languages/news_de.properties b/src/main/resources/languages/news_de.properties new file mode 100644 index 000000000..9911d5568 --- /dev/null +++ b/src/main/resources/languages/news_de.properties @@ -0,0 +1,15 @@ +news.further.information=Weitere Informationen +news.all.releases=Alle veröffentlichten und geplanten Versionen: +news.detailed=Ausführliches Changelog (nur auf Englisch): + +news.changeType.headline=Transaktionstyp ändern +news.changeType.description=Ermöglicht es eine Transaktion in einen anderen Typ umwandeln (z.B. eine normale Transaktion in eine wiederholende Transaktion ändern). + +news.readonlyAccounts.headline=Deaktivierbare Konten +news.readonlyAccounts.description=Konten können deaktiviert werden (verbietet Transaktionen hinzuzufügen oder zu löschen). + +news.firstUseWizard.headline=Hilfe zur ersten Benutzung +news.firstUseWizard.description=Einfache Einführung in die Benutzung von BudgetMaster. + +news.java11.headline=Java 11 +news.java11.description=Das gesamte Projekt wurde auf Java 11 migriert. \ No newline at end of file diff --git a/src/main/resources/languages/news_en.properties b/src/main/resources/languages/news_en.properties new file mode 100644 index 000000000..e88629deb --- /dev/null +++ b/src/main/resources/languages/news_en.properties @@ -0,0 +1,15 @@ +news.further.information=Further information +news.all.releases=All published and planned releases: +news.detailed=More detailed changelog (english only): + +news.changeType.headline=Change transaction type +news.changeType.description=Transform a transaction to another type (e.g. change a normal transaction to a repeating one). + +news.readonlyAccounts.headline=Readonly accounts +news.readonlyAccounts.description=Allow to deactivate accounts (transactions can't be added or removed from deactivated accounts). + +news.firstUseWizard.headline=First use wizard +news.firstUseWizard.description=Simple introduction on how to use BudgetMaster. + +news.java11.headline=Java 11 +news.java11.description=Migrate the project to Java 11. \ No newline at end of file diff --git a/src/main/resources/static/css/dark/hotkeys.css b/src/main/resources/static/css/dark/hotkeys.css index 82ea27853..2226e3b12 100644 --- a/src/main/resources/static/css/dark/hotkeys.css +++ b/src/main/resources/static/css/dark/hotkeys.css @@ -6,4 +6,8 @@ font-family: Consolas, "Courier New", monospace; display: inline-block; margin-right: 2rem; +} + +.modifier-key { + margin-right: 0; } \ No newline at end of file diff --git a/src/main/resources/static/css/dark/reports.css b/src/main/resources/static/css/dark/reports.css index d789ff430..4d03618e1 100644 --- a/src/main/resources/static/css/dark/reports.css +++ b/src/main/resources/static/css/dark/reports.css @@ -55,7 +55,20 @@ .table-advice { width: auto; - margin: auto; + margin: 1vmin; + border: 2px solid white; + border-radius: 5px; + padding: 0 1vmin; + display: inline-block; +} + +.table-advice td{ + padding: 10px; + font-size: 1.5vmin; +} + +.table-advice i { + font-size: 2.5vmin; } .columnName-selected { diff --git a/src/main/resources/static/css/dark/style.css b/src/main/resources/static/css/dark/style.css index 807ec5467..44a4b1448 100644 --- a/src/main/resources/static/css/dark/style.css +++ b/src/main/resources/static/css/dark/style.css @@ -201,6 +201,10 @@ ul.sidenav.sidenav-fixed > li:last-child { font-style: italic; } +.input-label { + color: #FFFFFF !important; +} + /* input text color */ .input-field input[type=text] { color: #FFFFFF; @@ -415,7 +419,34 @@ textarea { } #logo-home { - max-height: 15vmin; + max-height: 13vmin; +} + +.home-firstUseBanner-wrapper { + display: inline-block; +} + +.home-firstUseBanner { + border: 2px solid white; + border-radius: 5px; + padding: 0 1vmin; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + font-size: 1.8vmin; +} + +.home-firstUseBanner-item { + padding: 10px 0 10px 10px; +} + +.home-firstUseBanner i { + font-size: 2.5vmin; +} + +.home-firstUseBanner-clear i { + font-size: 1.8vmin; } .break-all { @@ -612,6 +643,16 @@ input[type="radio"]:not(:checked) + span::before, [type="radio"]:not(:checked) + color: #2E79B9; } +.placeholder-icon { + display: inline-block; + width: 1.3rem; + margin-right: 15px; +} + +.whatsNewLink:hover { + cursor: pointer; +} + .invisible { opacity: 0; } diff --git a/src/main/resources/static/css/dark/templates.css b/src/main/resources/static/css/dark/templates.css index 9e2b19049..2dfa2860d 100644 --- a/src/main/resources/static/css/dark/templates.css +++ b/src/main/resources/static/css/dark/templates.css @@ -3,7 +3,7 @@ } .template-header-name { - max-width: 60%; + max-width: 50%; } .collapsible-header-button { @@ -11,4 +11,8 @@ right: 15px; top: 8px; font-weight: bold; +} + +.template-selected { + background-color: #888888; } \ No newline at end of file diff --git a/src/main/resources/static/css/dark/transactions.css b/src/main/resources/static/css/dark/transactions.css index 867ccdad1..6f6161008 100644 --- a/src/main/resources/static/css/dark/transactions.css +++ b/src/main/resources/static/css/dark/transactions.css @@ -106,6 +106,11 @@ width: auto; } +#transaction-actions-button .mobile-fab-tip { + margin-right: 4rem; + right: 0; +} + #button-new-transaction { height: 36px; width: auto; diff --git a/src/main/resources/static/css/hotkeys.css b/src/main/resources/static/css/hotkeys.css index 82ea27853..2226e3b12 100644 --- a/src/main/resources/static/css/hotkeys.css +++ b/src/main/resources/static/css/hotkeys.css @@ -6,4 +6,8 @@ font-family: Consolas, "Courier New", monospace; display: inline-block; margin-right: 2rem; +} + +.modifier-key { + margin-right: 0; } \ No newline at end of file diff --git a/src/main/resources/static/css/reports.css b/src/main/resources/static/css/reports.css index b6d62562a..a12d47280 100644 --- a/src/main/resources/static/css/reports.css +++ b/src/main/resources/static/css/reports.css @@ -37,13 +37,26 @@ background-color: #EEEEEE; } -.columnName-disabled .columnName-label{ +.columnName-disabled .columnName-label { color: #878787; } .table-advice { width: auto; - margin: auto; + margin: 1vmin; + border: 2px solid #212121; + border-radius: 5px; + padding: 0 1vmin; + display: inline-block; +} + +.table-advice td { + padding: 10px; + font-size: 1.5vmin; +} + +.table-advice i { + font-size: 2.5vmin; } .columnName-selected { diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 8a988dd3d..ca7ed7049 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -184,6 +184,10 @@ ul.sidenav.sidenav-fixed > li:last-child font-style: italic; } +.input-label { + color: #2E79B9 !important; +} + /* label focus color */ .input-field input[type=text]:focus + label { color: #2E79B9 !important; @@ -362,7 +366,34 @@ ul.sidenav.sidenav-fixed > li:last-child } #logo-home { - max-height: 15vmin; + max-height: 13vmin; +} + +.home-firstUseBanner-wrapper { + display: inline-block; +} + +.home-firstUseBanner { + border: 2px solid #212121; + border-radius: 5px; + padding: 0 1vmin; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + font-size: 1.8vmin; +} + +.home-firstUseBanner-item { + padding: 10px 0 10px 10px; +} + +.home-firstUseBanner i { + font-size: 2.5vmin; +} + +.home-firstUseBanner-clear i { + font-size: 1.8vmin; } .break-all { @@ -527,6 +558,16 @@ input[type="radio"]:checked + span::after, [type="radio"].with-gap:checked + spa color: #2E79B9; } +.placeholder-icon { + display: inline-block; + width: 1.3rem; + margin-right: 15px; +} + +.whatsNewLink:hover { + cursor: pointer; +} + .invisible { opacity: 0; } diff --git a/src/main/resources/static/css/templates.css b/src/main/resources/static/css/templates.css index 2a9b13429..2d1c2225e 100644 --- a/src/main/resources/static/css/templates.css +++ b/src/main/resources/static/css/templates.css @@ -3,7 +3,7 @@ } .template-header-name { - max-width: 60%; + max-width: 50%; } .collapsible-header-button { @@ -13,3 +13,6 @@ font-weight: bold; } +.template-selected { + background-color: rgb(238, 238, 238); +} diff --git a/src/main/resources/static/css/transactions.css b/src/main/resources/static/css/transactions.css index 867ccdad1..6f6161008 100644 --- a/src/main/resources/static/css/transactions.css +++ b/src/main/resources/static/css/transactions.css @@ -106,6 +106,11 @@ width: auto; } +#transaction-actions-button .mobile-fab-tip { + margin-right: 4rem; + right: 0; +} + #button-new-transaction { height: 36px; width: auto; diff --git a/src/main/resources/static/js/about.js b/src/main/resources/static/js/about.js new file mode 100644 index 000000000..1e57ffcdf --- /dev/null +++ b/src/main/resources/static/js/about.js @@ -0,0 +1,7 @@ +$(document).ready(function() +{ + $('.whatsNewLink').click(function() + { + fetchAndShowWhatsNewModal(this, 'whatsNewModelContainerOnDemand'); + }); +}); diff --git a/src/main/resources/static/js/hotkeys.js b/src/main/resources/static/js/hotkeys.js index 675c86ecd..8b28bf96a 100644 --- a/src/main/resources/static/js/hotkeys.js +++ b/src/main/resources/static/js/hotkeys.js @@ -26,7 +26,7 @@ Mousetrap.bind('v', function() { if(areHotKeysEnabled()) { - window.location.href = rootURL + '/templates/select'; + window.location.href = rootURL + '/templates'; } }); @@ -55,19 +55,32 @@ Mousetrap.bind('esc', function() } }); +let saveTransactionOrTemplateButton = document.getElementById('button-save-transaction'); +if(saveTransactionOrTemplateButton !== null) +{ + Mousetrap(document.querySelector('body')).bind('mod+enter', function(e) + { + document.getElementById('button-save-transaction').click(); + }); +} function areHotKeysEnabled() { - return !isSearchFocused() && !isCategorySelectFocused(); + return !isSearchFocused() && !isCategorySelectFocused() && !isTemplateSearchFocused(); } - function isSearchFocused() { let searchElement = document.getElementById('search'); return document.activeElement === searchElement; } +function isTemplateSearchFocused() +{ + let templateSearchElement = document.getElementById('searchTemplate'); + return document.activeElement === templateSearchElement; +} + function isCategorySelectFocused() { let activeElement = document.activeElement; diff --git a/src/main/resources/static/js/main.js b/src/main/resources/static/js/main.js index 80dde13a8..d9ca05ab1 100644 --- a/src/main/resources/static/js/main.js +++ b/src/main/resources/static/js/main.js @@ -14,6 +14,11 @@ $(document).ready(function() $('#modalBackupReminder').modal('open'); } + if($("#whatsNewModelContainer").length) + { + fetchAndShowWhatsNewModal(document.getElementById('whatsNewModelContainer'), 'whatsNewModelContainer'); + } + $('.tooltipped').tooltip(); $('select').formSelect(); @@ -48,6 +53,30 @@ $(document).ready(function() }); }); + +function fetchAndShowWhatsNewModal(item, containerID) +{ + let modalID = '#modalWhatsNew'; + let modal = $(modalID).modal(); + if(modal.isOpen) + { + return; + } + + $.ajax({ + type: 'GET', + url: $(item).attr('data-url'), + data: {}, + success: function(data) + { + + $('#' + containerID).html(data); + $(modalID).modal(); + $(modalID).modal('open'); + } + }); +} + function addClass(element, className) { if(element != null) @@ -80,4 +109,4 @@ function rgb2hex(rgb) } return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); -} \ No newline at end of file +} diff --git a/src/main/resources/static/js/templates.js b/src/main/resources/static/js/templates.js index 822ea631c..cdcf18a2d 100644 --- a/src/main/resources/static/js/templates.js +++ b/src/main/resources/static/js/templates.js @@ -5,31 +5,6 @@ $(document).ready(function() $('#modalConfirmDelete').modal('open'); } - if($('#buttonSaveAsTemplate').length) - { - $('#buttonSaveAsTemplate').click(function() - { - // check if transaction form is valid - let isValidForm = validateForm(true); - if(!isValidForm) - { - $('#modalCreateFromTransaction').modal('close'); - M.toast({html: createTemplateWithErrorInForm}); - return; - } - - $.ajax({ - type: 'GET', - url: $('#buttonSaveAsTemplate').attr('data-url'), - data: {}, - success: function(data) - { - createAndOpenModal(data) - } - }); - }); - } - M.Collapsible.init(document.querySelector('.collapsible.expandable'), { accordion: false }); @@ -58,8 +33,17 @@ $(document).ready(function() { handleIncludeAccountCheckbox('include-transfer-account', 'transaction-transfer-account') } + + if($("#searchTemplate").length) + { + document.getElementById('searchTemplate').focus(); + } + + enableHotKeys(); }); +let selectedTemplateName = null; + function handleIncludeAccountCheckbox(checkboxID, selectID) { document.getElementById(checkboxID).addEventListener('change', (event) => @@ -72,125 +56,202 @@ function handleIncludeAccountCheckbox(checkboxID, selectID) }); } -function createAndOpenModal(data) +function searchTemplates(searchText) { - let modalID = '#modalCreateFromTransaction'; + searchText = searchText.trim(); + searchText = searchText.toLowerCase() - $('#saveAsTemplateModalContainer').html(data); - $(modalID).modal(); - $(modalID).modal('open'); - let templateNameInput = document.getElementById('template-name'); - templateNameInput.focus(); - $(templateNameInput).on('keypress', function(e) + let templateItems = document.querySelectorAll('.template-item'); + let collapsible = document.getElementById('templateCollapsible'); + + if(!searchText) { - let code = e.keyCode || e.which; - if(code === 13) + templateItems.forEach((item) => + { + collapsible.classList.remove('hidden'); + item.classList.remove('hidden'); + }); + return; + } + + let numberOfVisibleItems = 0; + for(let i = 0; i < templateItems.length; i++) + { + let item = templateItems[i]; + let templateName = item.querySelector('.template-header-name').innerText; + if(templateName.toLowerCase().includes(searchText)) { - saveAsTemplate(); + item.classList.remove('hidden'); + numberOfVisibleItems++; } - }); + else + { + item.classList.add('hidden'); + } + } + + // hide whole collapsible to prevent shadows from remaining visible + if(numberOfVisibleItems === 0) + { + collapsible.classList.add('hidden'); - $('#buttonCreateTemplate').click(function() + // hide all item selections + let templateItems = document.getElementsByClassName('template-item'); + for(let i = 0; i < templateItems.length; i++) + { + toggleItemSelection(templateItems[i], false); + } + selectedTemplateName = null; + } + else { - saveAsTemplate(); - }); + collapsible.classList.remove('hidden'); + } + + handleKeyUpOrDown(null); } -function saveAsTemplate() +function enableHotKeys() { - // validate template name - let templateName = document.getElementById('template-name').value; - let isValid = validateTemplateName(templateName); - if(!isValid) + Mousetrap.bind('up', function() { - return - } + handleKeyUpOrDown(true); + }); + + Mousetrap.bind('down', function() + { + handleKeyUpOrDown(false); + }); - let form = document.getElementsByName('NewTransaction')[0]; - form.appendChild(createAdditionalHiddenInput('templateName', templateName)); - form.appendChild(createAdditionalHiddenInput('includeCategory', document.getElementById('include-category').checked)); - form.appendChild(createAdditionalHiddenInput('includeAccount', document.getElementById('include-account').checked)); + Mousetrap.bind('enter', function() + { + if(!isSearchFocused()) + { + confirmTemplateSelection(); + } + }); - // replace form target url - form.action = $('#buttonCreateTemplate').attr('data-url'); - form.submit(); + handleKeyUpOrDown(false); } -function validateTemplateName(templateName) +function handleKeyUpOrDown(isUp) { - if(templateName.length === 0) + let templateItems = document.getElementsByClassName('template-item'); + for(let i = 0; i < templateItems.length; i++) { - addTooltip('template-name', templateNameEmptyValidationMessage); - return false; + toggleItemSelection(templateItems[i], false); } - else + templateItems = document.querySelectorAll('.template-item:not(.hidden)'); + + if(templateItems.length === 0) { - removeTooltip('template-name'); + selectedTemplateName = null; + return; } - if(existingTemplateNames.includes(templateName)) + let previousIndex = getIndexOfTemplateName(templateItems, selectedTemplateName); + let noItemSelected = selectedTemplateName === null; + let previousItemNoLongerInList = previousIndex === null; + + if(noItemSelected || previousItemNoLongerInList) { - addTooltip('template-name', templateNameDuplicateValidationMessage); - return false; + // select the first item + selectItem(templateItems, 0); } else { - removeTooltip('template-name'); - } + if(isUp === null ) + { + selectItem(templateItems, previousIndex); + return; + } - return true; + // select next item + if(isUp) + { + selectNextItemOnUp(templateItems, previousIndex); + } + else + { + selectNextItemOnDown(templateItems, previousIndex); + } + } } -function createAdditionalHiddenInput(name, value) +function selectItem(templateItems, index) { - let newInput = document.createElement('input'); - newInput.setAttribute('type', 'hidden'); - newInput.setAttribute('name', name); - newInput.setAttribute('value', value); - return newInput; + toggleItemSelection(templateItems[index], true); + selectedTemplateName = getTemplateName(templateItems[index]); + document.getElementById('searchTemplate').focus(); } -function searchTemplates(searchText) +function toggleItemSelection(templateItem, isSelected) { - searchText = searchText.trim(); - searchText = searchText.toLowerCase() + templateItem.getElementsByClassName('collapsible-header')[0].classList.toggle('template-selected', isSelected); +} - let templateItems = document.querySelectorAll('.template-item'); - let collapsible = document.getElementById('templateCollapsible'); +function getTemplateName(templateItem) +{ + return templateItem.getElementsByClassName('template-header-name')[0]; +} - if(!searchText) +function getIndexOfTemplateName(templateItems, templateName) +{ + for(let i = 0; i < templateItems.length; i++) { - templateItems.forEach((item) => + let currentTemplateName = getTemplateName(templateItems[i]); + if(currentTemplateName === templateName) { - collapsible.classList.remove('hidden'); - item.classList.remove('hidden'); - }); - return; + return i; + } } - let numberOfVisibleItems = 0; - for(let i = 0; i < templateItems.length; i++) + return null; +} + +function selectNextItemOnDown(templateItems, previousIndex) +{ + let isLastItemSelected = previousIndex + 1 === templateItems.length; + if(isLastItemSelected) { - let item = templateItems[i]; - let templateName = item.querySelector('.template-header-name').innerText; - if(templateName.toLowerCase().includes(searchText)) - { - item.classList.remove('hidden'); - numberOfVisibleItems++; - } - else - { - item.classList.add('hidden'); - } + selectItem(templateItems, 0); + } + else + { + selectItem(templateItems, previousIndex + 1); } +} - // hide whole collapsible to prevent shadows from remaining visible - if(numberOfVisibleItems === 0) +function selectNextItemOnUp(templateItems, previousIndex) +{ + let isFirstItemSelected = previousIndex === 0; + if(isFirstItemSelected) { - collapsible.classList.add('hidden'); + selectItem(templateItems, templateItems.length - 1); } else { - collapsible.classList.remove('hidden'); + selectItem(templateItems, previousIndex - 1); + } +} + +function confirmTemplateSelection() +{ + let templateItems = document.querySelectorAll('.template-item:not(.hidden)'); + if(templateItems.length === 0) + { + selectedTemplateName = null; + return; + } + + let index = getIndexOfTemplateName(templateItems, selectedTemplateName); + let indexItemNoLongerInList = index === null; + let noItemSelected = selectedTemplateName === null; + + if(noItemSelected || indexItemNoLongerInList) + { + return; } + + templateItems[index].getElementsByClassName('button-select-template')[0].click(); } \ No newline at end of file diff --git a/src/main/resources/static/js/transactionActions.js b/src/main/resources/static/js/transactionActions.js new file mode 100644 index 000000000..5b5e3b5cc --- /dev/null +++ b/src/main/resources/static/js/transactionActions.js @@ -0,0 +1,149 @@ +$(document).ready(function() +{ + M.FloatingActionButton.init(document.querySelectorAll('#transaction-actions-button'), {}); + + $('.transaction-action').click(function() + { + let actionType = $(this).attr('data-action-type'); + if(actionType === 'saveAsTemplate') + { + openSaveAsTemplateModal(this); + } + else if(actionType === 'changeType') + { + openChangeTransactionTypeModal(this); + } + }); +}); + +function openSaveAsTemplateModal(item) +{ + // check if transaction form is valid + let isValidForm = validateForm(true); + if(!isValidForm) + { + $('#modalCreateFromTransaction').modal('close'); + M.toast({html: createTemplateWithErrorInForm}); + return; + } + + $.ajax({ + type: 'GET', + url: $(item).attr('data-url'), + data: {}, + success: function(data) + { + createAndOpenModal(data) + } + }); +} + +function createAndOpenModal(data) +{ + let modalID = '#modalCreateFromTransaction'; + + $('#saveAsTemplateModalContainer').html(data); + $(modalID).modal(); + $(modalID).modal('open'); + let templateNameInput = document.getElementById('template-name'); + templateNameInput.focus(); + $(templateNameInput).on('keypress', function(e) + { + let code = e.keyCode || e.which; + if(code === 13) + { + saveAsTemplate(); + } + }); + + $('#buttonCreateTemplate').click(function() + { + saveAsTemplate(); + }); +} + +function saveAsTemplate() +{ + // validate template name + let templateName = document.getElementById('template-name').value; + let isValid = validateTemplateName(templateName); + if(!isValid) + { + return + } + + let form = document.getElementsByName('NewTransaction')[0]; + form.appendChild(createAdditionalHiddenInput('templateName', templateName)); + form.appendChild(createAdditionalHiddenInput('includeCategory', document.getElementById('include-category').checked)); + form.appendChild(createAdditionalHiddenInput('includeAccount', document.getElementById('include-account').checked)); + + // replace form target url + form.action = $('#buttonCreateTemplate').attr('data-url'); + form.submit(); +} + +function validateTemplateName(templateName) +{ + if(templateName.length === 0) + { + addTooltip('template-name', templateNameEmptyValidationMessage); + return false; + } + else + { + removeTooltip('template-name'); + } + + if(existingTemplateNames.includes(templateName)) + { + addTooltip('template-name', templateNameDuplicateValidationMessage); + return false; + } + else + { + removeTooltip('template-name'); + } + + return true; +} + +function createAdditionalHiddenInput(name, value) +{ + let newInput = document.createElement('input'); + newInput.setAttribute('type', 'hidden'); + newInput.setAttribute('name', name); + newInput.setAttribute('value', value); + return newInput; +} + +function openChangeTransactionTypeModal(item) +{ + $.ajax({ + type: 'GET', + url: $(item).attr('data-url'), + data: {}, + success: function(data) + { + createAndOpenModalSelectNewType(data) + } + }); +} + +function createAndOpenModalSelectNewType(data) +{ + let modalID = '#modalChangeTransactionType'; + + $('#changeTransactionTypeModalContainer').html(data); + $(modalID).modal(); + $(modalID).modal('open'); + $('#newTypeSelect').formSelect(); + + $('#buttonChangeTransactionType').click(function() + { + let newType = document.getElementById('newTypeSelect').value; + document.getElementById('inputNewType').setAttribute('value', newType); + + let form = document.getElementById('formChangeTransactionType'); + form.submit(); + }); +} \ No newline at end of file diff --git a/src/main/resources/static/js/transactions.js b/src/main/resources/static/js/transactions.js index 5c87becbb..a1b47400f 100644 --- a/src/main/resources/static/js/transactions.js +++ b/src/main/resources/static/js/transactions.js @@ -11,9 +11,13 @@ $(document).ready(function() if($("#transaction-name").length) { let elements = document.querySelectorAll('#transaction-name'); - M.Autocomplete.init(elements, { + let autoCompleteInstances = M.Autocomplete.init(elements, { data: transactionNameSuggestions, }); + + // prevent tab traversal for dropdown (otherwise "tab" needs to be hit twice to jump from name input to amount input) + autoCompleteInstances[0].dropdown.dropdownEl.tabIndex = -1; + document.getElementById('transaction-name').focus(); } @@ -22,6 +26,24 @@ $(document).ready(function() $("#transaction-description").characterCounter(); } + if($(".datepicker-simple".length) && $("#transaction-repeating-end-date-input").length) + { + let pickerEndDate = document.getElementById('transaction-repeating-end-date-input'); + + // select corresponding radio button + let endDate = document.getElementById("repeating-end-date"); + + pickerEndDate.addEventListener('input', function() + { + endDate.checked = true; + }); + + pickerEndDate.addEventListener('focus', function() + { + endDate.checked = true; + }); + } + if($(".datepicker").length) { let pickerStartDate = M.Datepicker.init(document.getElementById('transaction-datepicker'), { @@ -143,7 +165,8 @@ $(document).ready(function() if($(".chips-autocomplete").length) { - $('.chips-autocomplete').chips({ + let elements = document.querySelectorAll('.chips-autocomplete'); + let instances = M.Chips.init(elements, { autocompleteOptions: { data: tagAutoComplete, limit: Infinity, @@ -152,11 +175,19 @@ $(document).ready(function() placeholder: tagsPlaceholder, data: initialTags }); + + // prevent tab traversal for dropdown (otherwise "tab" needs to be hit twice to jump from tag input to account input) + instances[0].autocomplete.dropdown.dropdownEl.tabIndex = -1; } // prevent form submit on enter (otherwise tag functionality will be hard to use) $(document).on("keypress", 'form', function(e) { + if(e.ctrlKey) + { + return true; + } + let code = e.keyCode || e.which; if(code === 13) { @@ -208,7 +239,7 @@ $(document).ready(function() document.getElementById("input-isPayment").value = 1; }); - M.FloatingActionButton.init(document.querySelectorAll('.fixed-action-btn'), { + M.FloatingActionButton.init(document.querySelectorAll('.new-transaction-button'), { direction: 'bottom', hoverEnabled: false }); @@ -241,8 +272,12 @@ let transactionRepeatingEndAfterXTimesInputID = "#transaction-repeating-end-afte AMOUNT_REGEX = new RegExp("^-?\\d+(,\\d+)?(\\.\\d+)?$"); ALLOWED_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ",", "."]; +DATE_REGEX_SHORT_NO_DOTS = new RegExp("^\\d{6}$"); +DATE_REGEX_LONG_NO_DOTS = new RegExp("^\\d{8}$"); +DATE_REGEX_SHORT = new RegExp("^(\\d{2}.\\d{2}.)(\\d{2})$"); +DATE_REGEX_LONG = new RegExp("^\\d{2}.\\d{2}.\\d{4}$"); -function validateAmount(text, allowEmpty=false) +function validateAmount(text, allowEmpty = false) { let id = "transaction-amount"; @@ -268,7 +303,61 @@ function validateAmount(text, allowEmpty=false) } } -function validateForm(allowEmptyAmount=false) +function validateDate(inputId) +{ + let dateInput = document.getElementById(inputId); + dateInput.value = dateInput.value.trim(); + let date = dateInput.value; + + date = convertDateWithoutDots(date); + dateInput.value = date; + + if(date.match(DATE_REGEX_LONG) != null) + { + removeTooltip(inputId); + return true; + } + + let match = date.match(DATE_REGEX_SHORT); + if(match != null) + { + let dayAndMonth = match[1]; + let year = match[2]; + + let currentYear = new Date().getFullYear(); + currentYear = currentYear.toString().substr(0, 2); + + dateInput.value = dayAndMonth + currentYear + year; + removeTooltip(inputId); + return true; + } + else + { + addTooltip(inputId, dateValidationMessage); + return false; + } +} + +function convertDateWithoutDots(dateString) +{ + let yearLength = 2; + if(dateString.match(DATE_REGEX_SHORT_NO_DOTS) != null) + { + yearLength = 2; + } + else if(dateString.match(DATE_REGEX_LONG_NO_DOTS) != null) + { + yearLength = 4; + } + else + { + return dateString; + } + + return dateString.substr(0, 2) + '.' + dateString.substr(2, 2) + '.' + dateString.substr(4, yearLength); +} + +function validateForm(allowEmptyAmount = false) { // amount let isValidAmount = validateAmount($('#transaction-amount').val(), allowEmptyAmount); @@ -277,6 +366,13 @@ function validateForm(allowEmptyAmount=false) return false; } + // start date + let isValidDate = validateDate('transaction-datepicker'); + if(!isValidDate) + { + return false; + } + // description let description = document.getElementById('transaction-description').value; if(description.length > 250) @@ -329,6 +425,13 @@ function validateForm(allowEmptyAmount=false) if(endDate.checked) { + // start date + let isValidDate = validateDate('transaction-repeating-end-date-input'); + if(!isValidDate) + { + return false; + } + endInput.value = $("#transaction-repeating-end-date-input").val(); } } diff --git a/src/main/resources/templates/about.ftl b/src/main/resources/templates/about.ftl index 89a3b9a94..6230a55a8 100644 --- a/src/main/resources/templates/about.ftl +++ b/src/main/resources/templates/about.ftl @@ -6,6 +6,7 @@ <#import "helpers/navbar.ftl" as navbar> <@navbar.navbar "about" settings/> + <#import "/spring.ftl" as s> @@ -17,7 +18,12 @@ <@cellKey locale.getString("about.version")/> - ${build.getVersionName()} (${build.getVersionCode()}) + + ${build.getVersionName()} (${build.getVersionCode()}) + + ${locale.getString("about.version.whatsnew")}? + + <@cellKey locale.getString("about.date")/> @@ -45,6 +51,7 @@ <#import "helpers/scripts.ftl" as scripts> <@scripts.scripts/> +