diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index e08023aad4ac1596fbacc8970db351b9cca5861f..0000000000000000000000000000000000000000 Binary files a/.gitlab-ci.yml and /dev/null differ diff --git a/Dockerfile b/Dockerfile index 128d5b9ce131b68044d714774c2b20aae677a5ee..4f4eb221e97c66daf7d2c7712dde2b036c8e9a58 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 0ce08f5c9407edfdf3311ba14fb0cc1dec2bdac0..821e6792b03c16c07fdd6f29c88afa0376c325aa 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 af1b149277d4750ec36422446d88b2f5d76aea8c..0000000000000000000000000000000000000000 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 5a083d70f4e6c167237606c4a93b34585db96293..0000000000000000000000000000000000000000 --- 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 5bdc2063f0b8c4d842df250d41e71ea30040bc39..d653779ba98105816f67c356c0eb12e4d7fac9b7 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 3ee17912ac2197085d09c16dbfbd02bacc02125b..bbddde71cdaed4df16b4115e86332496308efb34 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 69a56d6b6b86f7bf1913d56a9c5371f4b8d31a34..dc238ce7fd6bb9fddece4e7900cc6fe78bf72656 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 5380589e95da9a88a74f5622afe658d5d0360b80..4ef472e4b4d96a4237ee1f9ca51579cbeae78195 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 a6ef90957278ee5fb054352057363dab2b5fcbaf..a6a9458b96c28e8fb042daff62acfe8fab686d12 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 9d6ddf292e2becc5c41835f994b414be3100d3c4..ea9de77956874593d10c3d81a68d8bcc695a3646 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 f3766d2bd2c591b76bf40783b8d387d7756e3f52..ae5f87d865086a70d6fabbdcadfbe150dc48f607 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 be19f7e567afbcf7ab207dd5335ccc9a1aca9bae..cad992f60e35e7679d577b72c92a413ec93c4220 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 45a8b5db651262af2235ea99c14a6b70b0e2ee9d..6a05cee43fdb1e538c05fcb76bd0ba9e49dcffd6 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 d8a673807d8babcf456310235f8b3131ad04edcb..eda6c5de1c33e247bed82b616c1c5b7119999f07 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 6865b5ac5fb87967fa62a07565b383f77f67f621..e4d659dba0132f615d728a57869076218da0a487 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 f1a6f9bfc9a9f10b4959779a93f102cdd30d9cbe..648a87fea550450a36b3f41b299b1edf7668a207 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 71d325577a9695b2aa6c5a2d5585e5dfec1dd592..e1341cd03654d2a968aa752abd376f45baae69b2 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 4ab0b885ff2c95afd5c6dd011dd7af35e54b3aca..c439a06c666fc7361e06cc544ca0f286796b08df 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 3bbf17017d1f5deb45f549e9651930bb70713a63..6da41fa1194460e119e604b41742dca42f80b377 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 7bd93970855031789aaedad9797667aab913bf05..42a6db44cc9eb0e26c31b57e6d41ecb2c905e2b8 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 c6583ec2097c969ec796249466d2d22e59aa31ac..58ed42492c671b3ee0be0120b5df3f35863d0d25 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 6280412768681318073478f64b40d311ebd0c891..c3a0e994e4f2d7218f420b2064bb909e508b9e39 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 0cbb5b675c41f2e26afc96bdaa027af5bf41a2d4..07745fcf1e7e24a7b0023987b42529fc2a745516 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 ee985aade20210a189615c0cbe357170144379b7..a0074e2b15d0ed3c91f1bcd51f187c249bbd4e5c 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 8f0b2d5212eb440af764f5648ed315bfa9cc9488..0000000000000000000000000000000000000000 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 1bddd35bf2a9ad6128f57824adb02cb1eb27d565..a404e406473a3355a999f84511c754a50d7e8115 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 b2ec964795214a240e9ae8245b17232b93449747..ba4183b6fc27a02d8a7188d48b2ead58b2347fc2 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 54d7ba010a925051e6cb743e4d8be2b81ab9a4b0..94a36483d55536933be3c3397d0deb6f4e6656a9 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 fc360eca577f26f0f0cb03c4953c0b2139d3f28d..1dff6f9f1f67df6fe5af27882ad32dc90ac8364c 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 a12d8ab09448e07dae4957a731245d6c48f69a25..c6cb8ab78d63ea280c7aec9a29bef328b0d78dcd 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 ce95eca270c6da232410793633c116dbf8bc2f4b..a2133e80c1c1d0926a1588d6e5fc62837b1f3e65 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 c3ecd69d295463b0ecec50cce6c27d908ad089bf..b440c9c6fbce127f65a59b088784ec6876f39f04 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 1d0cc77e885e98ad7d49662ed8dae9b7c581d8f7..2dee371acaf6873e3f5ce2408e67b2feb5a05287 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 87548a7ed96ce484c5b130a577e5902207c90872..eeea2d8dab8f03e7cced86be5e2b2cfe09fb51ec 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 4d644578d142e940af7a4d6f0f19b1c8fffde6a2..9580d595563186ccd185611a859dd17a3e5e557a 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 48b1724cf3d965ec807eca2b4450faaaffb0cf73..48c5985ac44f147288b8ef474f9c266e402f2871 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 7f073b2fdad99eefe25970a7f35c79860a44f23d..1d26edf205aaa615c4aca03ff14ba7ce873a7be0 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 66edaa9087076423d6bcf4c2d81b20085282a6b8..ba49581eef9a32d3527e990d160b89e3b8215f59 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 26889c76fada2ef94cdad1b26a2b415de9a847b9..13940d6a383882815009e28557c9c80c5e7cf576 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 14a434d8cdbcf39e0a0b31416d9aac334d96f1c1..39c45b824c60eb1144601f5c58003ddb125e17ab 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 2eec50146c70fe89b3c6e8f3b81285546ef943ae..b54cf43ee90463458d6fa674c749c972510cf90a 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 23273e9869297bf160cf69c77ac3cc957f6d1db4..83f10b217f80b065bedd416725deaab6e7ed379f 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 6ebc590b1593093ad2c8db17867091f0b33ba982..5ee707c97d77d806a5aa38b158b6e953222c7798 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 cd1c2a3fdbf1f665ac10aebd3c2fdbae16dd09de..ee2d86f107bfd2d05e1f6d71da35bded11d5da7a 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 232af88e4dee488b5c5abf5d650d034718a30116..a55ea76d7908d94a2df076c7867c2af0fc43f531 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 a25ea3342e6c60d65e1da01e8498c84a6e5b16b8..c2211e290546c62e2443cc33da2a182608b0fa7d 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 e7ccbada1ee2e42136b1a38093bf0d96d994c924..0000000000000000000000000000000000000000 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 ca39e3a06d71155c74beebb4cea65d9c1ceddf67..facf1bd4af646ca3834f4759edd704115e46ed1a 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 9f82d2adf5474754b08897bc7078f0d52579332c..fa143cb5b172a768318253d2c798e337b6825739 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ <groupId>de.deadlocker8</groupId> <artifactId>BudgetMaster</artifactId> - <version>2.4.5</version> + <version>2.5.0</version> <name>BudgetMaster</name> <repositories> @@ -35,7 +35,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>2.2.5.RELEASE</version> + <version>2.2.11.RELEASE</version> <relativePath/> </parent> @@ -54,23 +54,23 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> - <java.version>1.8</java.version> + <java.version>11</java.version> - <jlibs.version>2.0.6</jlibs.version> - <versionizer.version>1.2.1</versionizer.version> - <webjars-locator.version>0.39</webjars-locator.version> - <jquery.version>3.4.1</jquery.version> + <jlibs.version>3.2.0</jlibs.version> + <versionizer.version>3.0.1</versionizer.version> + <webjars-locator.version>0.40</webjars-locator.version> + <jquery.version>3.5.1</jquery.version> <materializecss.version>1.0.0</materializecss.version> - <fontawesome.version>5.12.0</fontawesome.version> - <sortablejs.version>1.8.3</sortablejs.version> - <mousetrap.version>1.6.1</mousetrap.version> + <fontawesome.version>5.15.1</fontawesome.version> + <sortablejs.version>1.10.2</sortablejs.version> + <mousetrap.version>1.6.5</mousetrap.version> <codemirror.version>5.50.0</codemirror.version> <selenium.version>3.141.59</selenium.version> - <assertj-core.version>3.15.0</assertj-core.version> + <assertj-core.version>3.17.1</assertj-core.version> <app.versionDate>${maven.build.timestamp}</app.versionDate> <maven.build.timestamp.format>dd.MM.yy</maven.build.timestamp.format> - <app.versionCode>28</app.versionCode> + <app.versionCode>29</app.versionCode> <app.author>Robert Goldmann</app.author> <project.outputDirectory>build/${project.version}</project.outputDirectory> @@ -245,7 +245,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> - <version>3.2.2</version> + <version>3.3.1</version> <configuration> <webappDirectory>${basedir}/src/main</webappDirectory> <outputDirectory>${project.outputDirectory}</outputDirectory> @@ -264,7 +264,7 @@ <plugin> <groupId>com.akathist.maven.plugins.launch4j</groupId> <artifactId>launch4j-maven-plugin</artifactId> - <version>1.7.21</version> + <version>1.7.25</version> <executions> <execution> <id>l4j-clui</id> @@ -283,7 +283,7 @@ <jre> <bundledJre64Bit>false</bundledJre64Bit> <bundledJreAsFallback>false</bundledJreAsFallback> - <minVersion>1.8.0</minVersion> + <minVersion>11</minVersion> <jdkPreference>preferJre</jdkPreference> <runtimeBits>64/32</runtimeBits> </jre> @@ -296,7 +296,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> - <version>2.12</version> + <version>2.22.1</version> <configuration> <junitArtifactName>junit:junit</junitArtifactName> <argLine>-Dfile.encoding=UTF-8</argLine> @@ -311,7 +311,7 @@ <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> - <version>1.10</version> + <version>1.12</version> <executions> <execution> <id>attach-artifacts</id> diff --git a/src/main/java/de/deadlocker8/budgetmaster/Main.java b/src/main/java/de/deadlocker8/budgetmaster/Main.java index 1ec0c1ab7dbcbdb0c628290727f5771812741353..77401e212f7add7fee5b87019aecfe66fe851e08 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.Files; 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 class Main extends SpringBootServletInitializer implements ApplicationRun } @Override - public String getBaseResource() + public String[] getBaseResources() { - return "languages/"; + return new String[]{"languages/base", "languages/news"}; } @Override @@ -57,10 +58,17 @@ public class Main extends SpringBootServletInitializer implements ApplicationRun { 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 @@ public class Main extends SpringBootServletInitializer implements ApplicationRun } 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 class Main extends SpringBootServletInitializer implements ApplicationRun 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 3a17d593c9ec131f694a1bfd6663f9f17334a6d2..f361954093d9760beff66e079add27b0c78e1af7 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<String> args = new ArrayList<>(); + private ProgramArgs() + { + } + public static void setArgs(List<String> 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 0a3fdb5250e93e8e492bf7eeb179ef08fe1090ef..fbbaa93ce43ab133c3d96fce1211d82c585ee2cf 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 class Account this.type = type; this.isSelected = false; this.isDefault = false; + this.isReadOnly = false; } public Account() @@ -95,6 +97,16 @@ public class Account isDefault = aDefault; } + public Boolean isReadOnly() + { + return isReadOnly; + } + + public void setReadOnly(Boolean readOnly) + { + isReadOnly = readOnly; + } + public AccountType getType() { return type; @@ -114,6 +126,7 @@ public class Account ", referringTransactions=" + referringTransactions + ", isSelected=" + isSelected + ", isDefault=" + isDefault + + ", isReadOnly=" + isReadOnly + ", type=" + type + '}'; } @@ -126,6 +139,7 @@ public class Account 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 class Account @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 df03dddd74c32d2a2f856298eff66cdc36eac5b9..54cd57729c96701988bd1900df9d675782f99d50 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 @@ package de.deadlocker8.budgetmaster.accounts; 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 class AccountController extends BaseController 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 class AccountController extends BaseController return "redirect:" + referer; } - @GetMapping("/accounts") + @GetMapping(value = "/{ID}/toggleReadOnly") + public String toggleReadOnly(HttpServletRequest request, @PathVariable("ID") Integer ID) + { + final Optional<Account> 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 class AccountController extends BaseController 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 class AccountController extends BaseController return "accounts/newAccount"; } - @GetMapping("/accounts/{ID}/edit") + @GetMapping("/{ID}/edit") public String editAccount(Model model, @PathVariable("ID") Integer ID) { - Optional<Account> accountOptional = accountRepository.findById(ID); - if(!accountOptional.isPresent()) + Optional<Account> accountOptional = accountService.getRepository().findById(ID); + if(accountOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -114,7 +135,7 @@ public class AccountController extends BaseController 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 class AccountController extends BaseController 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 class AccountController extends BaseController if(account.getID() == null) { // new account - accountRepository.save(account); + accountService.getRepository().save(account); } else { // edit existing account - Optional<Account> existingAccountOptional = accountRepository.findById(account.getID()); + Optional<Account> 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 01e72be7fe06ec009e99085392ae239ba01e974a..3536d8fb2fdfc4c09063e9cad1b1f8c62a32d76b 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<Account, Integer> { List<Account> findAllByTypeOrderByNameAsc(AccountType accountType); + List<Account> findAllByTypeAndIsReadOnlyOrderByNameAsc(AccountType accountType, Boolean isReadOnly); + Account findByName(String name); List<Account> 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 9f0c7c5222c725ac9dbf25878f09ffd1a844a0e4..a2c710af86432b813eef3b5238bede4831b9351b 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 @@ import java.util.Optional; @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 class AccountService implements Resetable return accounts; } + public List<Account> getAllActivatedAccountsAsc() + { + List<Account> accounts = accountRepository.findAllByType(AccountType.ALL); + accounts.addAll(accountRepository.findAllByTypeAndIsReadOnlyOrderByNameAsc(AccountType.CUSTOM, false)); + return accounts; + } + public void deleteAccount(int ID) { Optional<Account> accountToDeleteOptional = accountRepository.findById(ID); - if(!accountToDeleteOptional.isPresent()) + if(accountToDeleteOptional.isEmpty()) { return; } @@ -97,6 +105,16 @@ public class AccountService implements Resetable 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 class AccountService implements Resetable deselectAllAccounts(); Optional<Account> accountToSelectOptional = accountRepository.findById(ID); - if(!accountToSelectOptional.isPresent()) + if(accountToSelectOptional.isEmpty()) { return; } @@ -140,15 +158,20 @@ public class AccountService implements Resetable public void setAsDefaultAccount(int ID) { - unsetDefaultForAllAccounts(); - Optional<Account> 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 c17be2d87ce6b3396f0a6b1d3f63d944ffa40313..a6b133113bfe8125e76d67b45138ddd73d7f07eb 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 javax.servlet.http.HttpServletRequest; import java.util.Map; @Controller +@RequestMapping(Mappings.LOGIN) public class LoginController extends BaseController { - @GetMapping("/login") + @GetMapping public String login(HttpServletRequest request, Model model) { Map<String, String[]> 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 702d3b1ca73591e1478944ff8263c86d486cb0a8..8dc3b2cda5fb501f7a9c6f83b29537aea81b243b 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 @@ import org.springframework.stereotype.Service; @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 d824fbaed3abe90f0addbc4be0dd38e3f8668121..93def3e53ff92006958e9dc2e03de12b527b8e2c 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.controller.BaseController; 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 @@ import java.util.stream.Collectors; @Controller +@RequestMapping(Mappings.CATEGORIES) public class CategoryController extends BaseController { private static final String WHITE = "#FFFFFF"; @@ -34,7 +36,7 @@ public class CategoryController extends BaseController this.settingsService = settingsService; } - @GetMapping("/categories") + @GetMapping public String categories(Model model) { model.addAttribute("categories", categoryService.getAllCategories()); @@ -42,7 +44,7 @@ public class CategoryController extends BaseController 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 class CategoryController extends BaseController 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 class CategoryController extends BaseController 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 class CategoryController extends BaseController return "categories/newCategory"; } - @GetMapping("/categories/{ID}/edit") + @GetMapping("/{ID}/edit") public String editCategory(Model model, @PathVariable("ID") Integer ID) { - Optional<Category> categoryOptional = categoryService.getRepository().findById(ID); - if(!categoryOptional.isPresent()) + Optional<Category> categoryOptional = categoryService.findById(ID); + if(categoryOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -109,7 +111,7 @@ public class CategoryController extends BaseController 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 class CategoryController extends BaseController { 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 285a6743f39889f00d83c8f0ca714e8a537ffd16..34c2ec81c61109cfe29267ad186e4a2f5dc99005 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.slf4j.LoggerFactory; 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 class CategoryService implements Resetable createDefaults(); } - public CategoryRepository getRepository() + public Optional<Category> 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<Category> 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 class CategoryService implements Resetable @SuppressWarnings("OptionalIsPresent") public boolean isDeletable(Integer ID) { - Optional<Category> categoryOptional = getRepository().findById(ID); + Optional<Category> categoryOptional = findById(ID); if(categoryOptional.isPresent()) { return categoryOptional.get().getType() == CategoryType.CUSTOM; @@ -91,7 +101,9 @@ public class CategoryService implements Resetable public List<Category> 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 2cc0de75a68752a855861686f27ad0670376b8b8..32dd048050ad5d72bef9344843717fba94fe3e63 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.services.HelpersService; 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.Optional; 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 class ChartController extends BaseController this.transactionService = transactionService; } - @GetMapping("/charts") + @GetMapping public String charts(Model model) { List<Chart> charts = chartService.getRepository().findAllByOrderByNameAsc(); @@ -66,12 +68,12 @@ public class ChartController extends BaseController return "charts/charts"; } - @PostMapping(value = "/charts") + @PostMapping public String showChart(Model model, @ModelAttribute("NewChartSettings") ChartSettings chartSettings) { chartSettings.setFilterConfiguration(filterHelpersService.updateCategoriesAndTags(chartSettings.getFilterConfiguration())); Optional<Chart> chartOptional = chartService.getRepository().findById(chartSettings.getChartID()); - if(!chartOptional.isPresent()) + if(chartOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -88,7 +90,7 @@ public class ChartController extends BaseController return "charts/charts"; } - @GetMapping("/charts/manage") + @GetMapping("/manage") public String manage(Model model) { model.addAttribute("charts", chartService.getRepository().findAllByOrderByNameAsc()); @@ -96,7 +98,7 @@ public class ChartController extends BaseController return "charts/manage"; } - @GetMapping("/charts/newChart") + @GetMapping("/newChart") public String newChart(Model model) { Chart emptyChart = DefaultCharts.CHART_DEFAULT; @@ -105,11 +107,11 @@ public class ChartController extends BaseController return "charts/newChart"; } - @GetMapping("/charts/{ID}/edit") + @GetMapping("/{ID}/edit") public String editChart(Model model, @PathVariable("ID") Integer ID) { Optional<Chart> chartOptional = chartService.getRepository().findById(ID); - if(!chartOptional.isPresent()) + if(chartOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -119,7 +121,7 @@ public class ChartController extends BaseController 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 class ChartController extends BaseController 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 class ChartController extends BaseController 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 8eefb45734f6819cf409e1ccaaa691d4077ac9bd..5b97ebc34ea05ae86a63decbc13fa9e8448873d7 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.slf4j.LoggerFactory; 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 class ChartService implements Resetable { 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 @@ public class ChartService implements Resetable 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 8aaee465fbe3d63ff0ca80fe24392fa920ebc939..b2773215851a4175c529df13093a3c3571dac5cc 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 org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URL; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; @@ -72,7 +73,7 @@ public class DefaultCharts 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 b4f6c695c915f5518af55ab60a3306f3d95a5b58..de1ed12043c6c4b362bc2558a9d23f08579e6499 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 class AboutController extends BaseController 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<NewsEntry> 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 234c2913e1592483b827ea33122407023e2603f1..843f116ec02bfddf527def2d8d42acf3f6e3afd4 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 @@ import javax.servlet.http.HttpServletRequest; @Controller +@RequestMapping(Mappings.BACKUP_REMINDER) public class BackupController extends BaseController { private final SettingsService settingsService; @@ -20,7 +22,7 @@ public class BackupController extends BaseController this.settingsService = settingsService; } - @RequestMapping("/backupReminder/cancel") + @RequestMapping("/cancel") public String cancel(HttpServletRequest request, Model model) { settingsService.updateLastBackupReminderDate(); @@ -28,7 +30,7 @@ public class BackupController extends BaseController 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 95ae9cd60332352e1402d0f2e8d889f95e16d6c0..38ad6198c1b9262f93356b6c7640551c532cb78b 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 class HotKeysController extends BaseController 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 8b14238128188ea64865a2d3f135cf37e0e0e4c2..b2fe115a93c7b3cdc1c648b8134390ddde75ec8f 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 de.deadlocker8.budgetmaster.settings.SettingsService; 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 class IndexController extends BaseController 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 0000000000000000000000000000000000000000..c7ed16289f4c948599b695f79e9ce4a78aa3c52e --- /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 d5fc0e9478946ae87a6bc4850beb915bd45ad6b6..a273dd5e9b85d4e17deceb3746757c41c212f659 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 @@ import org.springframework.web.bind.annotation.RequestMapping; @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 e725f1f65dbb0282c0054f3bc2f193dbdca37180..4094442a6eafc0d0c3de79ba952b1bd9632eaa69 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 de.thecodelabs.utils.util.Localization; 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 class DatabaseParser } 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 dd52b178905a4df2d9719e30228e2b05e96d1a80..2e0a64fc14138632c41c20b0848246c8d0dba5eb 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 @@ public class DatabaseParser_v3 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 eeba1be6fd350a4d76886cec8a2db6eb1f5d780a..a80ebbec712772d56de2496bc08f42175dbf5d71 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.JsonObject; 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 class DatabaseParser_v4 extends DatabaseParser_v3 return new Database(categories, accounts, transactions, templates); } + @Override + protected List<Transaction> parseTransactions(JsonObject root) + { + List<Transaction> 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<Template> parseTemplates(JsonObject root) { final List<Template> parsedTemplates = new ArrayList<>(); @@ -77,6 +127,8 @@ public class DatabaseParser_v4 extends DatabaseParser_v3 final Optional<Integer> transferAccountOptional = parseIDOfElementIfExists(templateObject, "transferAccount"); transferAccountOptional.ifPresent(integer -> template.setTransferAccount(super.getAccountByID(integer))); + handleIsExpenditure(templateObject, template); + parsedTemplates.add(template); } @@ -90,6 +142,27 @@ public class DatabaseParser_v4 extends DatabaseParser_v3 { 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 9a824a2b8184e8fb618a5df31d9ece803f4d6c72..e14439f3deedba7f842db9125eb4e11d2996283c 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.io.Writer; 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 class DatabaseService final List<String> 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 class DatabaseService public Database getDatabaseForJsonSerialization() { - List<Category> categories = categoryService.getRepository().findAll(); + List<Category> categories = categoryService.getAllCategories(); List<Account> accounts = accountService.getRepository().findAll(); List<Transaction> transactions = transactionService.getRepository().findAll(); List<Transaction> 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 111f84d27e09c3aa6eb6fb42b4c1485ac1e049a1..88fcb674c7fa0b9a565cf752fcd8dd893be0a80a 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 @@ import org.springframework.web.context.request.WebRequest; @Controller +@RequestMapping(Mappings.FILTER) public class FilterController extends BaseController { private final FilterHelpersService filterHelpers; @@ -21,14 +23,14 @@ public class FilterController extends BaseController 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 50f72e93a50a21fae3d05c8a684386beaa97dce0..4dede91d31f08820ac57b4d2ebe05eae8f21d107 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 org.joda.time.DateTime; import javax.persistence.*; import java.util.List; +import java.util.Objects; @Entity public class RepeatingEndAfterXTimes extends RepeatingEnd @@ -32,4 +33,20 @@ public class RepeatingEndAfterXTimes extends RepeatingEnd { 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 3ac3a3194375794c3cf5597468d679e1e2b0a42a..a7dc605d5a0cadc99c011721871df140d2136e7f 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 org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; import java.util.List; +import java.util.Objects; @Entity public class RepeatingEndDate extends RepeatingEnd @@ -35,4 +36,20 @@ public class RepeatingEndDate extends RepeatingEnd { 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 81c7a9c02087b82d4a5fc7fb00fd7059a345caae..c56e5adf8b79c9d409985a79414593c3a7907af5 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 @@ package de.deadlocker8.budgetmaster.reports; 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 4479e516ace40b89d257a8a1f2ca589886fb0733..1645e87c974be6e79620cc779e2e3962cb17587e 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.services.HelpersService; 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.ServletOutputStream; 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 class ReportController extends BaseController 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 class ReportController extends BaseController 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 class ReportController extends BaseController .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 1000f39f4ba018ec6be6b790c8ce62e2035cc071..db5320d1bdf27a0b6760fb9be09131cdec7c3f69 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.categoryBudget.CategoryBudget; 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 org.springframework.stereotype.Service; 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 class ReportGeneratorService 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 c8877cceeee523592e7a6b969c9b3b2d0d5d312c..081d047f04316b4edb95fabb94b614544697fa03 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 @@ import java.util.*; public class CategoryBudgetHandler { + private CategoryBudgetHandler() + { + } + public static List<CategoryBudget> getCategoryBudgets(List<Transaction> transactions, List<Category> categories) { List<CategoryBudget> 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 62a3d43bdc16bab2e7b580e4baf2ae6143618eeb..f49e826bfb7734856b2adcbab2faf20fa95c41bc 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 @@ import org.springframework.stereotype.Service; @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 903ac1b73aed16d766dcedf6d4d29b1c5de71df8..8d6356736a32de78cd15c0f0999d4c72541fa84f 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 @@ import java.util.Optional; @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 4082dbca4f28c531346e855cae9ce49c4a5c875c..bb88ab1c45120b9bd4876ec6b90380dad60cf639 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.settings.SettingsService; 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.data.jpa.domain.Specification; 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 class SearchController extends BaseController 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 55fc1240a6ebf9ae42f31574e5f5ba353a052554..bdd296d24f18b3c5c04450403a67bdee065d1ada 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 50bf4d93b8b45af4ec9d9588621389b1dd9f9db7..c5b10116aa8648193f0c9e6a940ae9640e493e6a 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.deadlocker8.budgetmaster.utils.LanguageType; 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<LanguageType> getAvailableLanguages() { return Arrays.asList(LanguageType.values()); @@ -195,4 +199,9 @@ public class HelpersService { 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 e7b34f8e3b6940f360c2d6f9b3a845b102afd213..6546cf697efc81357a791d018042354bca19f60c 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.slf4j.LoggerFactory; 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 class ImportService private void importCategories() { List<Category> categories = database.getCategories(); - LOGGER.debug("Importing " + categories.size() + " categories..."); + LOGGER.debug(MessageFormat.format("Importing {0} categories...", categories.size())); List<TransactionBase> alreadyUpdatedTransactions = new ArrayList<>(); List<TransactionBase> 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 class ImportService private void importAccounts(AccountMatchList accountMatchList) { - LOGGER.debug("Importing " + accountMatchList.getAccountMatches().size() + " accounts..."); + LOGGER.debug(MessageFormat.format("Importing {0} accounts...", accountMatchList.getAccountMatches().size())); List<TransactionBase> alreadyUpdatedTransactions = new ArrayList<>(); List<TransactionBase> alreadyUpdatedTransferTransactions = new ArrayList<>(); List<TransactionBase> 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<TransactionBase> transactions = new ArrayList<>(database.getTransactions()); transactions.removeAll(alreadyUpdatedTransactions); @@ -155,7 +156,7 @@ public class ImportService alreadyUpdatedTransferTransactions.addAll(updateTransferAccountsForTransactions(transferTransactions, accountMatch.getAccountSource().getID(), accountMatch.getAccountDestination())); List<TransactionBase> 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 class ImportService private void importTransactions() { List<Transaction> 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 class ImportService private void importTemplates() { List<Template> 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 4b2c3326fa4f5366747a56893106d2cd8889aa6d..b17f89124cf5deeb4f73642ccb87e4a24e285ec1 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 class LocalizationService implements Localization.LocalizationDelegate } @Override - public String getBaseResource() + public String[] getBaseResources() { - return "languages/"; + return new String[]{"languages/base", "languages/news"}; } @Override @@ -40,4 +40,10 @@ public class LocalizationService implements Localization.LocalizationDelegate { 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 7d03c617cebee24cfbd5d6d749564bd51c171073..cc8edcb1207de674e68907b98b555052c2527336 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 class Settings defaultSettings.setAutoBackupTime(AutoBackupTime.TIME_00); defaultSettings.setAutoBackupFilesToKeep(3); defaultSettings.setInstalledVersionCode(0); + defaultSettings.setWhatsNewShownForCurrentVersion(false); + defaultSettings.setShowFirstUseBanner(true); return defaultSettings; } @@ -198,6 +202,31 @@ public class Settings 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 class Settings ", 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 b66606749bb8f06237fed2faafed42cd9543fb7b..8080f5179ce3eb70a836f972f077283b1e8af607 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.Database; 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.ServletOutputStream; 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<Integer> SEARCH_RESULTS_PER_PAGE_OPTIONS = Arrays.asList(10, 20, 25, 30, 50, 100); @Autowired @@ -71,7 +71,7 @@ public class SettingsController extends BaseController this.scheduleTaskService = scheduleTaskService; } - @GetMapping("/settings") + @GetMapping public String settings(WebRequest request, Model model) { model.addAttribute("settings", settingsService.getSettings()); @@ -85,7 +85,7 @@ public class SettingsController extends BaseController 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 @@ public class SettingsController extends BaseController return Optional.empty(); } - @GetMapping("/settings/database/requestExport") + @GetMapping("/database/requestExport") public void downloadFile(HttpServletResponse response) { LOGGER.debug("Exporting database..."); @@ -204,7 +204,7 @@ public class SettingsController extends BaseController } } - @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 class SettingsController extends BaseController 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 class SettingsController extends BaseController return "settings/settings"; } - @GetMapping("/settings/database/requestImport") + @GetMapping("/database/requestImport") public String requestImportDatabase(Model model) { model.addAttribute("importDatabase", true); @@ -247,7 +247,7 @@ public class SettingsController extends BaseController 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 class SettingsController extends BaseController 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 class SettingsController extends BaseController } } - @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 class SettingsController extends BaseController 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 27ea656ecf73247c595b7b1f3a0ae1ba70bbe5e3..1bc4e724cd5e2c7f54778104c8f1032b1663afc3 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 @@ import java.util.Optional; @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 class SettingsService @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 class SettingsService Settings defaultSettings = Settings.getDefault(); Optional<Settings> settingsOptional = settingsRepository.findById(0); - if(!settingsOptional.isPresent()) + if(settingsOptional.isEmpty()) { throw new RuntimeException("Missing Settings in database"); } @@ -81,6 +81,14 @@ public class SettingsService { 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 class SettingsService 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 18ba745530393602270bd22a69dd2dcb98b375d6..5cc67b22ed9175a2219992056785d44c0224f1d5 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 @@ import java.util.List; @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 46cd542b52cbbb4bd0880a04ad41009c289623ab..d1b8de81c3352cdb6cdbf0e0a1c1936d461a49d4 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 1f7a484fbf1903666dfbf56137b03a44f81e333a..57e710b7a3c831e00c776c8e1dbe72f59e63afed 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.services.DateService; 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.validation.BindingResult; 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 class TemplateController extends BaseController this.accountService = accountService; } - @GetMapping("/templates") + @GetMapping public String showTemplates(Model model) { model.addAttribute("settings", settingsService.getSettings()); @@ -54,22 +54,14 @@ public class TemplateController extends BaseController 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 class TemplateController extends BaseController return "redirect:/templates"; } - @GetMapping("/templates/{ID}/requestDelete") + @GetMapping("/{ID}/requestDelete") public String requestDeleteTemplate(Model model, @PathVariable("ID") Integer ID) { final Optional<Template> templateOptional = templateService.getRepository().findById(ID); - if(!templateOptional.isPresent()) + if(templateOptional.isEmpty()) { throw new ResourceNotFoundException(); } @@ -105,35 +97,44 @@ public class TemplateController extends BaseController 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<Template> 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 class TemplateController extends BaseController 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 class TemplateController extends BaseController if(template.isExpenditure() == null) { - template.setIsExpenditure(true); + template.setIsExpenditure(false); } if(template.getAmount() != null) @@ -187,7 +188,7 @@ public class TemplateController extends BaseController 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 class TemplateController extends BaseController return "redirect:/templates"; } - @GetMapping("/templates/{ID}/edit") + @GetMapping("/{ID}/edit") public String editTemplate(Model model, @PathVariable("ID") Integer ID) { Optional<Template> 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 b11fd292de6c80c286129f8e5e01ddc105884ae4..3c41b17d95231fe534fece7d4fdcb058c86ba5ba 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.services.Resetable; 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 class TemplateService implements Resetable 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 class TemplateService implements Resetable 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<Account> accounts) diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionBase.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionBase.java index 5589d577ffc1df5b1ef8aa7282837c6e589e53ba..30b07d66d8904fbd29211d412efed2227e89c3fa 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<Tag> getTags(); void setTags(List<Tag> tags); diff --git a/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionController.java b/src/main/java/de/deadlocker8/budgetmaster/transactions/TransactionController.java index acb0b578b6fc57b6f18930c32184059c498682fa..20dcc22b4aba8595992141ef403cdc13963efd99 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.DateFormatStyle; 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.validation.BindingResult; 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 class TransactionController extends BaseController 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 class TransactionController extends BaseController 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 @@ public class TransactionController extends BaseController 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 class TransactionController extends BaseController 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 class TransactionController extends BaseController 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 @@ public class TransactionController extends BaseController 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 @@ public class TransactionController extends BaseController return "redirect:/transactions"; } - @GetMapping("/transactions/{ID}/edit") + @GetMapping("/{ID}/edit") public String editTransaction(Model model, @CookieValue("currentDate") String cookieDate, @PathVariable("ID") Integer ID) { Optional<Transaction> 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 class TransactionController extends BaseController } 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 class TransactionController extends BaseController 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 class TransactionController extends BaseController model.addAttribute("highlightID", ID); return "transactions/transactions"; } + + @GetMapping("/{ID}/changeTypeModal") + public String changeTypeModal(Model model, @PathVariable("ID") Integer ID) + { + final Optional<Transaction> 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<Transaction> transactionOptional = transactionService.getRepository().findById(ID); + if(transactionOptional.isEmpty()) + { + throw new ResourceNotFoundException(); + } + + final Optional<TransactionType> 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 969d49b94f410ac5a4f87e93999e20ebaf5acf88..52d401fbc59d930f7f8dcd52935e2c31d27660ff 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.Gson; 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.data.jpa.domain.Specification; 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 @@ public class TransactionService implements Resetable List<Transaction> 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 class TransactionService implements Resetable private void deleteTransactionInRepo(Integer ID) { Optional<Transaction> 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 class TransactionService implements Resetable 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 @@ public class TransactionService implements Resetable return item; } - public void prepareModelNewOrEdit(Model model, boolean isEdit, DateTime date, TransactionBase item, List<Account> accounts) + public void prepareModelNewOrEdit(Model model, boolean isEdit, DateTime date, TransactionType previousType, TransactionBase item, List<Account> 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 61a44cb69c641838e5bb6621190f82130a2430ae..325747e7100b246d5f393e4a627cd65c850408b7 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 @@ import java.util.List; public class TransactionSpecifications { + private TransactionSpecifications() + { + } + public static Specification<Transaction> 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 0000000000000000000000000000000000000000..2bae53042f594201e80f4c66595b777868e6c072 --- /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<TransactionType> 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 39023c02ff91ab44ec7bfcd4c7cf060869a173fb..a7d397b7009dd7504248a6cd6c4fb03f972af353 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 org.springframework.stereotype.Service; import java.io.File; import java.nio.file.Paths; +import java.text.MessageFormat; @Service public class BudgetMasterUpdateService @@ -79,7 +80,7 @@ public class BudgetMasterUpdateService { 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 0000000000000000000000000000000000000000..ad3c5dd04b04a082b6a902b38958a26e1946cb88 --- /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 eeca9389ea8a32565f68551403087b4e8b9664d8..6b875bd1f856dc4202bc2d725f89fbbc4d0a4f91 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 class UpdateInstalledVersion 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 86714465b268382e60d1a7d518f168c765c021b6..229d7bb78c7639fc00e17ee456b8833a8c70df50 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 <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a> 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.<br>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.<br>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.<br>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 a963468530558b1698bea13bb2e9312ac81f82c7..c8eb808b3a4a1df6f635e2ba10cb8e3d4ccd6ba9 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.<br>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.<br>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.<br>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 0000000000000000000000000000000000000000..9911d5568eb9935f38d01819f9a9222d6364432e --- /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 0000000000000000000000000000000000000000..e88629deb78cbdf12c182d525b99cf66b359cf9c --- /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 82ea278531de25ca236d1a5fdd9bc551a20357aa..2226e3b12cc908595de2b5ac55d1a3a3a1b2d2d1 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 d789ff4300d73ef55753c49fa2861e4da9a86a74..4d03618e141bb27c886f06d3e968d539d40798a8 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 807ec54678debe16167d482021e808f7694ad402..44a4b14484318154ae6e918822807f2ddbb767fa 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 9e2b1904902c700b06a7d91bb83bf76dfb4f4101..2dfa2860d7a48c0e9d325040c34e38adda159829 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 867ccdad1dc1d66d648e5aa8d33525b966e5bb31..6f616100810e8c3cc6e98f2b76276680e467cf16 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 82ea278531de25ca236d1a5fdd9bc551a20357aa..2226e3b12cc908595de2b5ac55d1a3a3a1b2d2d1 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 b6d62562a2f8532993100af5b3eea883596132d9..a12d472802f23c63321c6cf7eaa5f29c12020dbd 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 8a988dd3d872fc44588d62f39ac768d8ac8a81ea..ca7ed7049cca5e3b30899643b15eba92d5b74eef 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 2a9b1342932341fef8bd25f5b1e262d2e2edf3c5..2d1c2225e43413cde60fe6ff915adb8073185411 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 867ccdad1dc1d66d648e5aa8d33525b966e5bb31..6f616100810e8c3cc6e98f2b76276680e467cf16 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 0000000000000000000000000000000000000000..1e57ffcdf9e6df998b1d91c742cc3d051575695d --- /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 675c86ecd6f70ae6cbc18b9488ebbbfa03d495b2..8b28bf96ad69bcb0b3381742cb7006d32470b1b7 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 80dde13a8e43d80704cbd0ceeebb8a9cba9d4ca1..d9ca05ab1bc9601af74f6ca9bc311832a317e127 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 822ea631cf2679fe348b973681035ffe763a3ab1..cdcf18a2d60cc3603d496c562c857e9670e45b41 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 0000000000000000000000000000000000000000..5b5e3b5cc5ab078dd59046b4384bcfdee6dfe69d --- /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 5c87becbb3255a975ee87629fcac90a1a0b0d640..a1b47400f55978bb5d7900d235e106dfcbe3f30e 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 89a3b9a9473bb5ed743c7ef69f45256cec54cad3..6230a55a86e1a72da2a3c98ca698dec7f5bcc784 100644 --- a/src/main/resources/templates/about.ftl +++ b/src/main/resources/templates/about.ftl @@ -6,6 +6,7 @@ <body class="budgetmaster-blue-light"> <#import "helpers/navbar.ftl" as navbar> <@navbar.navbar "about" settings/> + <#import "/spring.ftl" as s> <main> <div class="card main-card background-color"> @@ -17,7 +18,12 @@ <div class="hide-on-small-only"><br><br></div> <div class="row"> <@cellKey locale.getString("about.version")/> - <div class="col s8 m5 l5">${build.getVersionName()} (${build.getVersionCode()})</div> + <div class="col s8 m5 l5"> + ${build.getVersionName()} (${build.getVersionCode()}) + + <a class="whatsNewLink" data-url="<@s.url '/about/whatsNewModal'/>">${locale.getString("about.version.whatsnew")}?</a> + <div id="whatsNewModelContainerOnDemand"></div> + </div> </div> <div class="row"> <@cellKey locale.getString("about.date")/> @@ -45,6 +51,7 @@ <!-- Scripts--> <#import "helpers/scripts.ftl" as scripts> <@scripts.scripts/> + <script src="<@s.url '/js/about.js'/>"></script> </body> </html> diff --git a/src/main/resources/templates/accounts/accounts.ftl b/src/main/resources/templates/accounts/accounts.ftl index a8a8377485e37bdb8f672832811d60438e41ca1b..e0aaf64843f4eef044406efe6ffbd6ffb8e9c264 100644 --- a/src/main/resources/templates/accounts/accounts.ftl +++ b/src/main/resources/templates/accounts/accounts.ftl @@ -16,15 +16,27 @@ </div> </div> <br> - <div class="center-align"><a href="<@s.url '/accounts/newAccount'/>" class="waves-effect waves-light btn budgetmaster-blue"><i class="material-icons left">add</i>${locale.getString("title.account.new")}</a></div> + <div class="center-align"><a href="<@s.url '/accounts/newAccount'/>" id="button-new-account" class="waves-effect waves-light btn budgetmaster-blue"><i class="material-icons left">add</i>${locale.getString("title.account.new")}</a></div> <br> - <div class="container"> + <div class="container account-container"> <table class="bordered"> <#list accounts as account> <#if (account.getType().name() == "CUSTOM")> <tr> <td> - <a href="<@s.url '/accounts/${account.getID()?c}/setAsDefault'/>" class="btn-flat no-padding text-color tooltipped" data-position="left" data-tooltip="${locale.getString("account.tooltip.default")}"><i class="material-icons left"><#if account.isDefault()>star<#else>star_border</#if></i></a> + <#if account.isReadOnly()> + <#assign toolTipText = locale.getString("account.tooltip.readonly.activate")/> + <#assign lockIcon = '<i class="fas fa-lock"></i>'/> + <div class="placeholder-icon"></div> + <#else> + <#assign toolTipText = locale.getString("account.tooltip.readonly.deactivate")/> + <#assign lockIcon = '<i class="fas fa-lock-open"></i>'/> + <a href="<@s.url '/accounts/${account.getID()?c}/setAsDefault'/>" class="btn-flat no-padding text-color tooltipped" data-position="left" data-tooltip="${locale.getString("account.tooltip.default")}"><i class="material-icons left"><#if account.isDefault()>star<#else>star_border</#if></i></a> + </#if> + + <#if !account.isDefault()> + <a href="<@s.url '/accounts/${account.getID()?c}/toggleReadOnly'/>" class="btn-flat no-padding text-color tooltipped" data-position="right" data-tooltip="${toolTipText}">${lockIcon}</a> + </#if> </td> <td>${account.getName()}</td> <td> diff --git a/src/main/resources/templates/accounts/newAccount.ftl b/src/main/resources/templates/accounts/newAccount.ftl index fe5560aa58b21dec88698471b9f6d6cf2991858f..671d1a9016c9d9cefb196726a4fa88bad1443a10 100644 --- a/src/main/resources/templates/accounts/newAccount.ftl +++ b/src/main/resources/templates/accounts/newAccount.ftl @@ -22,6 +22,7 @@ <input type="hidden" name="ID" value="<#if account.getID()??>${account.getID()?c}</#if>"> <input type="hidden" name="isSelected" value="<#if account.isSelected()??>${account.isSelected()?c}</#if>"> <input type="hidden" name="isDefault" value="<#if account.isDefault()??>${account.isDefault()?c}</#if>"> + <input type="hidden" name="isReadOnly" value="<#if account.isReadOnly()??>${account.isReadOnly()?c}</#if>"> <#-- name --> <div class="row"> @@ -39,7 +40,7 @@ </div> <div class="col s6 left-align"> - <button class="btn waves-effect waves-light budgetmaster-blue" type="submit" name="action"> + <button id="button-save-account" class="btn waves-effect waves-light budgetmaster-blue" type="submit" name="action"> <i class="material-icons left">save</i>${locale.getString("save")} </button> </div> @@ -52,7 +53,7 @@ </div> <div class="row center-align"> <div class="col s12"> - <button class="btn waves-effect waves-light budgetmaster-blue" type="submit" name="buttonSave"> + <button id="button-save-account" class="btn waves-effect waves-light budgetmaster-blue" type="submit" name="buttonSave"> <i class="material-icons left">save</i>${locale.getString("save")} </button> </div> diff --git a/src/main/resources/templates/firstUse.ftl b/src/main/resources/templates/firstUse.ftl new file mode 100644 index 0000000000000000000000000000000000000000..89b5a3b7f204dd370cb6f6b0a8045ef146fb4a46 --- /dev/null +++ b/src/main/resources/templates/firstUse.ftl @@ -0,0 +1,115 @@ +<html> + <head> + <#import "helpers/header.ftl" as header> + <@header.header "BudgetMaster"/> + <#import "/spring.ftl" as s> + </head> + <body class="budgetmaster-blue-light"> + <#import "helpers/navbar.ftl" as navbar> + <@navbar.navbar "firstUseGuide" settings/> + + <#import "indexFunctions.ftl" as indexFunctions> + + <main> + <div class="card main-card background-color"> + <div class="container"> + <div class="section center-align"> + <div class="headline"><i class="fas fa-graduation-cap"></i> ${locale.getString("home.first.use")}</div> + </div> + </div> + <br> + + <div class="container"> + <div class="container center-align"> + <div class="row left-align"> + <div class="col s12"> + <@indexFunctions.stepContent headline="home.first.use.step.1.headline" contentText="home.first.use.step.1.contentText" actionUrl="/accounts" actionName="home.menu.accounts.action.manage"/> + </div> + </div> + <hr> + + <div class="row left-align"> + <div class="col s12"> + <@indexFunctions.stepContent headline="home.first.use.step.2.headline" contentText="home.first.use.step.2.contentText" actionUrl="/categories" actionName="home.menu.categories.action.manage"/> + </div> + </div> + <hr> + + <div class="row left-align"> + <div class="col s12"> + <@indexFunctions.stepContent headline="home.first.use.step.3.headline" contentText="home.first.use.step.3.contentText" actionUrl="/transactions/newTransaction/normal" actionName="home.menu.transactions.action.new"> + <ul class="browser-default"> + <li>${locale.getString("home.first.use.step.3.sub.1")}</li> + <li>${locale.getString("home.first.use.step.3.sub.2")}</li> + <li>${locale.getString("home.first.use.step.3.sub.3")}</li> + <li>${locale.getString("home.first.use.step.3.sub.4")}</li> + <li>${locale.getString("home.first.use.step.3.sub.5")}</li> + <li>${locale.getString("home.first.use.step.3.sub.6")}</li> + </ul> + </@indexFunctions.stepContent> + </div> + </div> + <hr> + + <div class="row left-align"> + <div class="col s12"> + <@indexFunctions.stepContent headline="home.first.use.step.4.headline" contentText="home.menu.transactions" actionUrl="/transactions" actionName="home.menu.transactions.action.manage"> + <br> + ${locale.getString("home.first.use.step.4.contentText")} + <ul class="browser-default"> + <li>${locale.getString("home.first.use.step.4.sub.1")}</li> + <li>${locale.getString("home.first.use.step.4.sub.2")}</li> + <li>${locale.getString("home.first.use.step.4.sub.3")}</li> + </ul> + </@indexFunctions.stepContent> + </div> + </div> + <hr> + + <div class="row left-align"> + <div class="col s12"> + <@indexFunctions.stepContent headline="home.first.use.step.5.headline" contentText="home.first.use.step.5.contentText" actionUrl="" actionName=""> + <h5>${locale.getString("menu.templates")}</h5> + <p> + ${locale.getString("home.first.use.step.5.sub.1")} + </p> + <p> + <@indexFunctions.action url="/templates" name="home.menu.templates.action.manage"/> + </p> + + <h5>${locale.getString("menu.charts")}</h5> + <p> + ${locale.getString("home.first.use.step.5.sub.2")} + </p> + <p> + <@indexFunctions.action url="/charts/manage" name="home.menu.charts.action.manage"/> + </p> + + <h5>${locale.getString("menu.reports")}</h5> + <p> + ${locale.getString("home.first.use.step.5.sub.3")} + </p> + <p> + <@indexFunctions.action url="/reports" name="home.menu.reports.action.new"/> + </p> + + <h5>${locale.getString("home.first.use.step.5.sub.4")}</h5> + + <p class="center-align"> + <a href="<@s.url '/'/>" class="waves-effect waves-light btn budgetmaster-blue"> + <i class="material-icons left">home</i>${locale.getString("home.first.use.home")} + </a> + </p> + </@indexFunctions.stepContent> + </div> + </div> + </div> + </div> + </div> + </main> + + <!-- Scripts--> + <#import "helpers/scripts.ftl" as scripts> + <@scripts.scripts/> + </body> +</html> diff --git a/src/main/resources/templates/helpers/header.ftl b/src/main/resources/templates/helpers/header.ftl index 32f53897f0640c7e9f7ff485e4267f99c6257579..c9f77b6cd2e771280c7e97de614b54fb98f4386d 100644 --- a/src/main/resources/templates/helpers/header.ftl +++ b/src/main/resources/templates/helpers/header.ftl @@ -11,7 +11,7 @@ <#import "/spring.ftl" as s> <title>${title}</title> <meta charset="UTF-8"/> - <link rel="stylesheet" href="<@s.url '/webjars/font-awesome/5.12.0/css/all.min.css'/>"> + <link rel="stylesheet" href="<@s.url '/webjars/font-awesome/5.15.1/css/all.min.css'/>"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link rel="stylesheet" href="<@s.url "/webjars/materializecss/1.0.0/css/materialize.min.css"/>"> <@style "style"/> diff --git a/src/main/resources/templates/helpers/navbar.ftl b/src/main/resources/templates/helpers/navbar.ftl index 246b29142d03649faa31901fa6490805403feaa1..0f770b5539cc16c3ead85ad9918efffa09ced5bd 100644 --- a/src/main/resources/templates/helpers/navbar.ftl +++ b/src/main/resources/templates/helpers/navbar.ftl @@ -18,6 +18,7 @@ <@itemDivider/> <@itemWithIcon "hotkeys", "/hotkeys", locale.getString("menu.hotkeys"), "keyboard", "budgetmaster-grey", activeID/> + <@itemWithFontawesomeIcon "firstUseGuide", "/firstUse", locale.getString("menu.firstUseGuide"), "fas fa-graduation-cap", "budgetmaster-grey", activeID/> <@itemWithIcon "about", "/about", locale.getString("menu.about"), "info", "budgetmaster-grey", activeID/> <@itemDivider/> @@ -25,7 +26,7 @@ <#if updateCheckService.isUpdateAvailable()> <@itemDivider/> - <@itemUpdate "/update", locale.getString("menu.update"), "system_update"/> + <@itemUpdate "/settings/update", locale.getString("menu.update"), "system_update"/> </#if> <#if programArgs.isTest()> @@ -50,6 +51,7 @@ </form> <@backupReminder settings/> + <@whatsNewModal settings/> </#macro> <#macro itemLogo> @@ -118,6 +120,14 @@ </#if> </#macro> +<#macro itemWithFontawesomeIcon ID link text icon activeColor activeID> + <#if activeID == ID> + <li class="active"><a href="<@s.url '${link}'/>" class="waves-effect no-padding"><div class="stripe ${activeColor}"></div><i class="${icon}"></i>${text}</a></li> + <#else> + <li><a href="<@s.url '${link}'/>" class="waves-effect"><i class="${icon}"></i>${text}</a></li> + </#if> +</#macro> + <#macro itemLogout text icon> <li><a class="waves-effect" id="button-logout"><i class="material-icons">${icon}</i>${text}</a></li> </#macro> @@ -143,4 +153,10 @@ </div> </div> </#if> +</#macro> + +<#macro whatsNewModal settings> + <#if settings.needToShowWhatsNew()> + <div id="whatsNewModelContainer" data-url="<@s.url '/about/whatsNewModal'/>"></div> + </#if> </#macro> \ No newline at end of file diff --git a/src/main/resources/templates/helpers/scripts.ftl b/src/main/resources/templates/helpers/scripts.ftl index 072702884a187f47300d7921806c830041ec9ba7..eb063fabd35ea8a0dba7f9ea4b09a9b907616bf1 100644 --- a/src/main/resources/templates/helpers/scripts.ftl +++ b/src/main/resources/templates/helpers/scripts.ftl @@ -1,8 +1,8 @@ <#macro scripts> <#import "/spring.ftl" as s> -<script src="<@s.url '/webjars/jquery/3.4.1/jquery.min.js'/>"></script> +<script src="<@s.url '/webjars/jquery/3.5.1/jquery.min.js'/>"></script> <script src="<@s.url '/webjars/materializecss/1.0.0/js/materialize.min.js'/>"></script> -<script src="<@s.url '/webjars/mousetrap/1.6.1/mousetrap.js'/>"></script> +<script src="<@s.url '/webjars/mousetrap/1.6.5/mousetrap.js'/>"></script> <script> rootURL = "<@s.url ''/>" </script> diff --git a/src/main/resources/templates/hotkeys.ftl b/src/main/resources/templates/hotkeys.ftl index b4d2fbc2ec021d79924cd13eba92d841872c8ccf..5126e551f7059db810887e9a9eb56254b634e9a2 100644 --- a/src/main/resources/templates/hotkeys.ftl +++ b/src/main/resources/templates/hotkeys.ftl @@ -34,6 +34,10 @@ <@cellKey locale.getString("hotkeys.transactions.new.template.key")/> <div class="col s8 m5 l5">${locale.getString("hotkeys.transactions.new.template")}</div> </div> + <div class="row"> + <@cellKeyWithModifier locale.getString("hotkeys.transactions.save.modifier") locale.getString("hotkeys.transactions.save.key")/> + <div class="col s8 m5 l5">${locale.getString("hotkeys.transactions.save")}</div> + </div> <div class="row"> <@cellKey locale.getString("hotkeys.transactions.filter.key")/> <div class="col s8 m5 l5">${locale.getString("hotkeys.transactions.filter")}</div> @@ -55,4 +59,12 @@ <div class="col s4 m3 offset-m2 l2 offset-l3 right-align bold"> <div class="keyboard-key">${key}</div> </div> +</#macro> + +<#macro cellKeyWithModifier modifier key> + <div class="col s4 m3 offset-m2 l2 offset-l3 right-align bold"> + <div class="keyboard-key modifier-key">${modifier}</div> + <span class="bold">+</span> + <div class="keyboard-key">${key}</div> + </div> </#macro> \ No newline at end of file diff --git a/src/main/resources/templates/index.ftl b/src/main/resources/templates/index.ftl index 2b513c373520d2199c4f72d85fc6b8042811da6a..c7f67f48be85b899574052f19675bfbc08213538 100644 --- a/src/main/resources/templates/index.ftl +++ b/src/main/resources/templates/index.ftl @@ -22,6 +22,10 @@ </div> </div> + <#if settings.getShowFirstUseBanner()> + <@indexFunctions.firstUseBanner/> + </#if> + <div class="hide-on-small-only"><br></div> <div class="row home-menu-flex"> diff --git a/src/main/resources/templates/indexFunctions.ftl b/src/main/resources/templates/indexFunctions.ftl index 5b11a9a284cb62b78c786e4e4cf56f681b8433b0..c438cf124a6eb308cc67b4af1eb55e61ad700b7f 100644 --- a/src/main/resources/templates/indexFunctions.ftl +++ b/src/main/resources/templates/indexFunctions.ftl @@ -16,4 +16,35 @@ <#macro action url name> <a href="<@s.url url/>" class="waves-effect btn-flat home-menu-link-item"><i class="material-icons left">play_arrow</i>${locale.getString(name)}</a> +</#macro> + +<#macro stepContent headline contentText actionUrl, actionName> + <h5>${locale.getString(headline)}</h5> + <p> + ${locale.getString(contentText)} + <#nested> + </p> + <p> + <#if actionUrl?has_content> + <@indexFunctions.action url=actionUrl name=actionName/> + </#if> + </p> +</#macro> + +<#macro firstUseBanner> + <div class="row" id="firstUseBanner"> + <div class="col s12 center-align"> + <div class="home-firstUseBanner-wrapper"> + <div class="home-firstUseBanner text-color"> + <a href="<@s.url "/firstUse"/>" class="text-color"> + <i class="fas fa-graduation-cap home-firstUseBanner-item"></i> + <span class="home-firstUseBanner-item">${locale.getString("home.first.use.teaser")}</span> + </a> + <a href="<@s.url "/settings/hideFirstUseBanner"/>" class="text-color home-firstUseBanner-item home-firstUseBanner-clear"> + <i class="material-icons">clear</i> + </a> + </div> + </div> + </div> + </div> </#macro> \ No newline at end of file diff --git a/src/main/resources/templates/reports/reports.ftl b/src/main/resources/templates/reports/reports.ftl index 314acf1ce80e422663efc0d5ab67662ed6b9e5f4..56081a90e0ecba4d284c453e3d3166366c4303b7 100644 --- a/src/main/resources/templates/reports/reports.ftl +++ b/src/main/resources/templates/reports/reports.ftl @@ -111,7 +111,7 @@ <!-- Scripts--> <#import "../helpers/scripts.ftl" as scripts> <@scripts.scripts/> - <script src="<@s.url '/webjars/sortablejs/1.8.3/Sortable.min.js'/>"></script> + <script src="<@s.url '/webjars/sortablejs/1.10.2/Sortable.min.js'/>"></script> <script src="<@s.url '/js/reports.js'/>"></script> <script src="<@s.url '/js/globalDatePicker.js'/>"></script> <script src="<@s.url '/js/filter.js'/>"></script> diff --git a/src/main/resources/templates/settings/settings.ftl b/src/main/resources/templates/settings/settings.ftl index f1d99668a0761f33a1927e4aee98553953ea9ea7..95c6b03835fcab6af25a36d9b788434058e4054a 100644 --- a/src/main/resources/templates/settings/settings.ftl +++ b/src/main/resources/templates/settings/settings.ftl @@ -25,6 +25,8 @@ <input type="hidden" name="ID" value="${settings.getID()?c}"> <input type="hidden" name="lastBackupReminderDate" value="${dateService.getLongDateString(settings.getLastBackupReminderDate())}"> <input type="hidden" name="installedVersionCode" value="${settings.getInstalledVersionCode()}"> + <input type="hidden" name="whatsNewShownForCurrentVersion" value="${settings.getWhatsNewShownForCurrentVersion()?c}"> + <input type="hidden" name="showFirstUseBanner" value="${settings.getShowFirstUseBanner()?c}"> <#-- password --> <div class="row"> @@ -182,7 +184,7 @@ </div> <div class="table-cell table-cell-valign"> - <a href="<@s.url '/updateSearch'/>" class="waves-effect waves-light btn budgetmaster-blue"><i class="material-icons left">refresh</i>${locale.getString("settings.updates.search")}</a> + <a href="<@s.url '/settings/updateSearch'/>" class="waves-effect waves-light btn budgetmaster-blue"><i class="material-icons left">refresh</i>${locale.getString("settings.updates.search")}</a> </div> </div> </div> diff --git a/src/main/resources/templates/settings/settingsMacros.ftl b/src/main/resources/templates/settings/settingsMacros.ftl index 02ca61e35d12b11c54cd5b9aebfdb3db61f3090f..ae293635762203971d514e2191a3e9f08567d1f7 100644 --- a/src/main/resources/templates/settings/settingsMacros.ftl +++ b/src/main/resources/templates/settings/settingsMacros.ftl @@ -140,7 +140,7 @@ </div> <div class="modal-footer background-color"> <a href="<@s.url '/settings'/>" class="modal-action modal-close waves-effect waves-light red btn-flat white-text">${locale.getString("cancel")}</a> - <a href="<@s.url '/performUpdate'/>" class="modal-action modal-close waves-effect waves-light green btn-flat white-text">${locale.getString("settings.update.start")}</a> + <a href="<@s.url '/settings/performUpdate'/>" class="modal-action modal-close waves-effect waves-light green btn-flat white-text">${locale.getString("settings.update.start")}</a> </div> </div> </#macro> \ No newline at end of file diff --git a/src/main/resources/templates/templates/selectTemplate.ftl b/src/main/resources/templates/templates/selectTemplate.ftl deleted file mode 100644 index c78ad19c4b6bb5fb792cf0d62905e692e55c6df5..0000000000000000000000000000000000000000 --- a/src/main/resources/templates/templates/selectTemplate.ftl +++ /dev/null @@ -1,47 +0,0 @@ -<html> - <head> - <#import "../helpers/header.ftl" as header> - <@header.header "BudgetMaster"/> - <@header.style "collapsible"/> - <@header.style "templates"/> - <#import "/spring.ftl" as s> - </head> - <body class="budgetmaster-blue-light"> - <#import "../helpers/navbar.ftl" as navbar> - <@navbar.navbar "templates" settings/> - - <#import "templateFunctions.ftl" as templateFunctions> - <#import "../categories/categoriesFunctions.ftl" as categoriesFunctions> - - <main> - <div class="card main-card background-color"> - <div class="container"> - <div class="section center-align"> - <div class="headline">${locale.getString("menu.templates")}</div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">search</i> - <input id="searchTemplate" type="text"> - <label for="searchTemplate">${locale.getString("search")}</label> - </div> - </div> - </div> - <br> - <div class="center-align"><a href="<@s.url '/templates'/>" class="waves-effect waves-light btn budgetmaster-blue"><i class="material-icons left">edit</i>${locale.getString("home.menu.templates.action.manage")}</a></div> - <br> - <#if templates?size == 0> - <div class="container"> - <div class="headline center-align">${locale.getString("placeholder")}</div> - </div> - <#else> - <@templateFunctions.listTemplates templates false/> - </#if> - </div> - </main> - - <#import "../helpers/scripts.ftl" as scripts> - <@scripts.scripts/> - <script src="<@s.url '/js/templates.js'/>"></script> - </body> -</html> \ No newline at end of file diff --git a/src/main/resources/templates/templates/templateFunctions.ftl b/src/main/resources/templates/templates/templateFunctions.ftl index 1bea158764588079690891d3bcfa15fd70940728..2f15b605a847382241a069e39f257ce40cff13d5 100644 --- a/src/main/resources/templates/templates/templateFunctions.ftl +++ b/src/main/resources/templates/templates/templateFunctions.ftl @@ -14,7 +14,7 @@ </div> </#macro> -<#macro listTemplates templates isEditable> +<#macro listTemplates templates> <div class="container"> <div class="row"> <div class="col s12"> @@ -24,12 +24,9 @@ <div class="collapsible-header bold"> <@templateHeader template/> <div class="collapsible-header-button"> - <#if isEditable> - <a href="<@s.url '/templates/${template.ID?c}/edit'/>" class="btn-flat no-padding text-color"><i class="material-icons left">edit</i></a> - <a href="<@s.url '/templates/${template.ID?c}/requestDelete'/>" class="btn-flat no-padding text-color"><i class="material-icons left">delete</i></a> - <#else> - <a href="<@s.url '/templates/${template.ID?c}/select'/>" class="waves-effect waves-light btn budgetmaster-blue"><i class="material-icons left">note_add</i>${locale.getString("title.transaction.new", locale.getString("title.transaction.new.normal"))}</a> - </#if> + <a href="<@s.url '/templates/${template.ID?c}/edit'/>" class="btn-flat no-padding text-color"><i class="material-icons left no-margin">edit</i></a> + <a href="<@s.url '/templates/${template.ID?c}/requestDelete'/>" class="btn-flat no-padding text-color"><i class="material-icons left no-margin">delete</i></a> + <a href="<@s.url '/templates/${template.ID?c}/select'/>" class="waves-effect waves-light btn budgetmaster-blue button-select-template"><i class="material-icons left no-margin">note_add</i></a> </div> </div> <div class="collapsible-body"> diff --git a/src/main/resources/templates/templates/templates.ftl b/src/main/resources/templates/templates/templates.ftl index 1197603600543609cf0a266152755e8212a9d722..6d87982fab93fdc237eb56f9cc4cbd951c531cae 100644 --- a/src/main/resources/templates/templates/templates.ftl +++ b/src/main/resources/templates/templates/templates.ftl @@ -22,7 +22,7 @@ <div class="row"> <div class="input-field col s12 m12 l8 offset-l2"> <i class="material-icons prefix">search</i> - <input id="searchTemplate" type="text"> + <input id="searchTemplate" type="text" class="mousetrap"> <label for="searchTemplate">${locale.getString("search")}</label> </div> </div> @@ -35,7 +35,7 @@ <div class="headline center-align">${locale.getString("placeholder")}</div> </div> <#else> - <@templateFunctions.listTemplates templates true/> + <@templateFunctions.listTemplates templates/> </#if> </div> diff --git a/src/main/resources/templates/transactions/changeTypeModal.ftl b/src/main/resources/templates/transactions/changeTypeModal.ftl new file mode 100644 index 0000000000000000000000000000000000000000..7afe21baeff0bcb6d68eee16c20e1928d297f77a --- /dev/null +++ b/src/main/resources/templates/transactions/changeTypeModal.ftl @@ -0,0 +1,38 @@ +<#global locale = static["de.thecodelabs.utils.util.Localization"]> +<#import "/spring.ftl" as s> + +<div id="modalChangeTransactionType" class="modal background-color"> + <div class="modal-content"> + <h4>${locale.getString("transaction.change.type")}</h4> + + <div class="row"> + <div class="sol s12"> + ${locale.getString("transaction.change.type.warning")} + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <select id="newTypeSelect"> + <#if transaction.isRepeating() || transaction.isTransfer()> + <option value="1">${locale.getString("title.transaction.new.normal")}</option> + </#if> + <#if !transaction.isRepeating()> + <option value="2">${locale.getString("title.transaction.new.repeating")}</option> + </#if> + <#if !transaction.isTransfer()> + <option value="3">${locale.getString("title.transaction.new.transfer")}</option> + </#if> + </select> + <label for="newTypeSelect">${locale.getString("transaction.change.type.new")}</label> + </div> + </div> + </div> + <div class="modal-footer background-color"> + <a class="modal-action modal-close waves-effect waves-light red btn-flat white-text">${locale.getString("cancel")}</a> + <a id="buttonChangeTransactionType" class="modal-action waves-effect waves-light green btn-flat white-text">${locale.getString("ok")}</a> + </div> + + <form id="formChangeTransactionType" class="hidden" action="<@s.url '/transactions/${transaction.getID()?c}/changeType'/>"> + <input type="hidden" name="newType" id="inputNewType"> + </form> +</div> \ No newline at end of file diff --git a/src/main/resources/templates/transactions/newTransactionMacros.ftl b/src/main/resources/templates/transactions/newTransactionMacros.ftl index 731f3c2bf52c05e4200df6af8ca6c827d2ac5682..16fd544584a228d1f3b1e312ebaf042c4979a2ca 100644 --- a/src/main/resources/templates/transactions/newTransactionMacros.ftl +++ b/src/main/resources/templates/transactions/newTransactionMacros.ftl @@ -56,7 +56,7 @@ <div class="row"> <div class="input-field col s12 m12 l8 offset-l2"> <input class="autocomplete" autocomplete="off" id="transaction-name" type="text" name="name" <@validation.validation "name"/> value="<#if transaction.getName()??>${transaction.getName()}</#if>"> - <label for="transaction-name">${locale.getString("transaction.new.label.name")}</label> + <label class="input-label" for="transaction-name">${locale.getString("transaction.new.label.name")}</label> </div> </div> @@ -73,7 +73,7 @@ <div class="row"> <div class="input-field col s12 m12 l8 offset-l2"> <input id="transaction-amount" type="text" <@validation.validation "amount"/> value="<#if transaction.getAmount()??>${currencyService.getAmountString(transaction.getAmount())}</#if>"> - <label for="transaction-amount">${locale.getString("transaction.new.label.amount")}</label> + <label class="input-label" for="transaction-amount">${locale.getString("transaction.new.label.amount")}</label> </div> <input type="hidden" id="hidden-transaction-amount" name="amount" value="<#if transaction.getAmount()??>${transaction.getAmount()}</#if>"> </div> @@ -81,6 +81,7 @@ <script> amountValidationMessage = "${locale.getString("warning.transaction.amount")}"; numberValidationMessage = "${locale.getString("warning.empty.number")}"; + dateValidationMessage = "${locale.getString("warning.transaction.date")}"; </script> </#macro> @@ -113,7 +114,7 @@ <option value="${category.getID()?c}">${categoryInfos}</option> </#list> </select> - <label for="transaction-category">${labelText}</label> + <label class="input-label" for="transaction-category">${labelText}</label> </div> </div> @@ -136,8 +137,8 @@ <#assign startDate = dateService.getLongDateString(currentDate)/> </#if> - <input id="transaction-datepicker" type="text" class="datepicker" name="date" value="${startDate}"> - <label for="transaction-datepicker">${locale.getString("transaction.new.label.date")}</label> + <input id="transaction-datepicker" type="text" class="datepicker<#if helpers.isUseSimpleDatepickerForTransactions()>-simple</#if>" name="date" value="${startDate}"> + <label class="input-label" for="transaction-datepicker">${locale.getString("transaction.new.label.date")}</label> </div> </div> @@ -151,7 +152,7 @@ <div class="row"> <div class="input-field col s12 m12 l8 offset-l2"> <textarea id="transaction-description" class="materialize-textarea" name="description" data-length="250" <@validation.validation "description"/>><#if transaction.getDescription()??>${transaction.getDescription()}</#if></textarea> - <label for="transaction-description">${locale.getString("transaction.new.label.description")}</label> + <label class="input-label" for="transaction-description">${locale.getString("transaction.new.label.description")}</label> </div> </div> </#macro> @@ -159,7 +160,7 @@ <#macro transactionTags transaction> <div class="row"> <div class="col s12 m12 l8 offset-l2"> - <label class="chips-label" for="transaction-chips">${locale.getString("transaction.new.label.tags")}</label> + <label class="input-label" class="chips-label" for="transaction-chips">${locale.getString("transaction.new.label.tags")}</label> <div id="transaction-chips" class="chips chips-placeholder chips-autocomplete"></div> </div> <div id="hidden-transaction-tags"></div> @@ -187,7 +188,7 @@ <#macro account accounts selectedAccount id name label disabled> <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> + <div class="input-field col s12 m12 l8 offset-l2" id="accountWrapper"> <select id="${id}" name="${name}" <@validation.validation "account"/> <#if disabled>disabled</#if>> <#list accounts as account> <#if (account.getType().name() != "CUSTOM")> @@ -202,7 +203,7 @@ <option value="${account.getID()?c}">${account.getName()}</option> </#list> </select> - <label for="${id}">${label}</label> + <label class="input-label" for="${id}">${label}</label> </div> </div> </#macro> @@ -329,7 +330,7 @@ <td class="cell">${locale.getString("repeating.end.date")}</td> <td class="cell input-cell"> <div class="input-field no-margin"> - <input class="datepicker no-margin input-min-width" id="transaction-repeating-end-date-input" type="text" value="${endDate}"> + <input class="datepicker<#if helpers.isUseSimpleDatepickerForTransactions()>-simple</#if> no-margin input-min-width" id="transaction-repeating-end-date-input" type="text" value="${endDate}"> <label for="transaction-repeating-end-date-input"></label> </div> </td> @@ -370,15 +371,31 @@ </#macro> <#macro buttonSave> - <button class="btn waves-effect waves-light budgetmaster-blue" type="submit" name="action"> + <button id="button-save-transaction" class="btn waves-effect waves-light budgetmaster-blue" type="submit" name="action"> <i class="material-icons left">save</i>${locale.getString("save")} </button> </#macro> -<#macro buttonTemplate> - <div class="fixed-action-btn"> - <a id="buttonSaveAsTemplate" class="btn-floating btn-large waves-effect waves-light budgetmaster-blue tooltipped" data-position="left" data-tooltip="${locale.getString("save.as.template")}" data-url="<@s.url '/templates/fromTransactionModal'/>"> - <i class="material-icons left">file_copy</i>${locale.getString("save")} - </a> - </div> +<#macro buttonTransactionActions canChangeType canCreateTemplate changetypeInProgress> + <#if (canChangeType || canCreateTemplate) && !changetypeInProgress> + <div class="fixed-action-btn" id="transaction-actions-button"> + <a class="btn-floating btn-large waves-effect waves-light budgetmaster-blue"> + <i class="material-icons left">settings</i>${locale.getString("save")} + </a> + <ul> + <#if canChangeType> + <li> + <a class="btn-floating btn transaction-action mobile-fab-tip no-wrap" data-action-type="changeType" data-url="<@s.url '/transactions/${transaction.getID()?c}/changeTypeModal'/>">${locale.getString("transaction.change.type")}</a> + <a class="btn-floating btn transaction-action budgetmaster-baby-blue" data-action-type="changeType" data-url="<@s.url '/transactions/${transaction.getID()?c}/changeTypeModal'/>"><i class="material-icons">shuffle</i></a> + </li> + </#if> + <#if canCreateTemplate> + <li> + <a class="btn-floating btn transaction-action mobile-fab-tip no-wrap" data-action-type="saveAsTemplate" data-url="<@s.url '/templates/fromTransactionModal'/>">${locale.getString("save.as.template")}</a> + <a class="btn-floating btn transaction-action budgetmaster-dark-orange" data-action-type="saveAsTemplate" data-url="<@s.url '/templates/fromTransactionModal'/>"><i class="material-icons">file_copy</i></a> + </li> + </#if> + </ul> + </div> + </#if> </#macro> \ No newline at end of file diff --git a/src/main/resources/templates/transactions/newTransactionNormal.ftl b/src/main/resources/templates/transactions/newTransactionNormal.ftl index c9ff04c77e8cbed71e894e8e5e9ba5b090e514dd..b10d4e96e38ea88bebf773f1e302ab9ed71863c9 100644 --- a/src/main/resources/templates/transactions/newTransactionNormal.ftl +++ b/src/main/resources/templates/transactions/newTransactionNormal.ftl @@ -29,6 +29,7 @@ <!-- only set ID for transactions not templates, otherwise the input is filled with the template ID and saving the transaction may then override an existing transactions if the ID is also already used in transactions table --> <input type="hidden" name="ID" value="<#if transaction.class.simpleName == "Transaction" && transaction.getID()??>${transaction.getID()?c}</#if>"> + <input type="hidden" name="previousType" value="<#if previousType??>${previousType.name()}</#if>"> <#-- isPayment switch --> <@newTransactionMacros.isExpenditureSwitch transaction/> @@ -61,10 +62,12 @@ <br> <#-- buttons --> <@newTransactionMacros.buttons "/transactions"/> - <@newTransactionMacros.buttonTemplate/> + <@newTransactionMacros.buttonTransactionActions isEdit true previousType??/> </form> <div id="saveAsTemplateModalContainer"></div> + + <div id="changeTransactionTypeModalContainer"></div> </div> </div> </main> @@ -85,6 +88,7 @@ <script src="<@s.url '/js/libs/spectrum.js'/>"></script> <script src="<@s.url '/js/helpers.js'/>"></script> <script src="<@s.url '/js/transactions.js'/>"></script> + <script src="<@s.url '/js/transactionActions.js'/>"></script> <script src="<@s.url '/js/categorySelect.js'/>"></script> <script src="<@s.url '/js/templates.js'/>"></script> </body> diff --git a/src/main/resources/templates/transactions/newTransactionRepeating.ftl b/src/main/resources/templates/transactions/newTransactionRepeating.ftl index 0362a0c29e709d66f5917d909f5b3271281539f0..a62a7ee6a9845cc2f061773a5620e402131ffab5 100644 --- a/src/main/resources/templates/transactions/newTransactionRepeating.ftl +++ b/src/main/resources/templates/transactions/newTransactionRepeating.ftl @@ -27,6 +27,7 @@ <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <input type="hidden" name="ID" value="<#if transaction.getID()??>${transaction.getID()?c}</#if>"> <input type="hidden" name="isRepeating" value="${transaction.isRepeating()?c}"> + <input type="hidden" name="previousType" value="<#if previousType??>${previousType.name()}</#if>"> <#-- isPayment switch --> <@newTransactionMacros.isExpenditureSwitch transaction/> @@ -61,7 +62,10 @@ <br> <#-- buttons --> <@newTransactionMacros.buttons "/transactions"/> + <@newTransactionMacros.buttonTransactionActions isEdit false previousType??/> </form> + + <div id="changeTransactionTypeModalContainer"></div> </div> </div> </main> @@ -76,6 +80,7 @@ <script src="<@s.url '/js/libs/spectrum.js'/>"></script> <script src="<@s.url '/js/helpers.js'/>"></script> <script src="<@s.url '/js/transactions.js'/>"></script> + <script src="<@s.url '/js/transactionActions.js'/>"></script> <script src="<@s.url '/js/categorySelect.js'/>"></script> </body> </html> diff --git a/src/main/resources/templates/transactions/newTransactionTransfer.ftl b/src/main/resources/templates/transactions/newTransactionTransfer.ftl index 7eeb0e0336b7da9f9c4d6926d783cc30882734b5..7c875bad4f5bdf312745b8631e69301e39d9c581 100644 --- a/src/main/resources/templates/transactions/newTransactionTransfer.ftl +++ b/src/main/resources/templates/transactions/newTransactionTransfer.ftl @@ -30,6 +30,7 @@ may then override an existing transactions if the ID is also already used in transactions table --> <input type="hidden" name="ID" value="<#if transaction.class.simpleName == "Transaction" && transaction.getID()??>${transaction.getID()?c}</#if>"> <input type="hidden" name="isExpenditure" value="true"> + <input type="hidden" name="previousType" value="<#if previousType??>${previousType.name()}</#if>"> <#-- name --> <@newTransactionMacros.transactionName transaction suggestionsJSON/> @@ -65,11 +66,13 @@ <br> <#-- buttons --> - <@newTransactionMacros.buttons "/transactions"/> - <@newTransactionMacros.buttonTemplate/> + <@newTransactionMacros.buttons '/transactions'/> + <@newTransactionMacros.buttonTransactionActions isEdit true previousType??/> </form> <div id="saveAsTemplateModalContainer"></div> + + <div id="changeTransactionTypeModalContainer"></div> </div> </div> </main> @@ -90,6 +93,7 @@ <script src="<@s.url '/js/libs/spectrum.js'/>"></script> <script src="<@s.url '/js/helpers.js'/>"></script> <script src="<@s.url '/js/transactions.js'/>"></script> + <script src="<@s.url '/js/transactionActions.js'/>"></script> <script src="<@s.url '/js/categorySelect.js'/>"></script> <script src="<@s.url '/js/templates.js'/>"></script> </body> diff --git a/src/main/resources/templates/transactions/transactionsMacros.ftl b/src/main/resources/templates/transactions/transactionsMacros.ftl index 74e1440f67d8f2cdeff731d71d6b8b473cafacd0..7b0beadbad5eaca799d51b33893a910be3894228 100644 --- a/src/main/resources/templates/transactions/transactionsMacros.ftl +++ b/src/main/resources/templates/transactions/transactionsMacros.ftl @@ -54,7 +54,7 @@ <#macro transactionButtons transaction> <div class="col s8 l2 xl1 right-align transaction-buttons no-wrap"> - <#if (transaction.category.type.name() != "REST")> + <#if (transaction.category.type.name() != "REST") && !transaction.getAccount().isReadOnly()> <a href="<@s.url '/transactions/${transaction.ID?c}/edit'/>" class="btn-flat no-padding text-color"><i class="material-icons left">edit</i></a> <a href="<@s.url '/transactions/${transaction.ID?c}/requestDelete'/>" class="btn-flat no-padding text-color"><i class="material-icons left no-margin">delete</i></a> </#if> @@ -139,8 +139,8 @@ </a> <ul class="${listClasses}"> <li> - <a href="<@s.url '/templates/select'/>" class="btn-floating btn budgetmaster-baby-blue"><i class="material-icons">file_copy</i></a> - <a href="<@s.url '/templates/select'/>" class="btn-floating btn mobile-fab-tip no-wrap">${locale.getString("title.transaction.new.from.template")}</a> + <a href="<@s.url '/templates'/>" class="btn-floating btn budgetmaster-baby-blue"><i class="material-icons">file_copy</i></a> + <a href="<@s.url '/templates'/>" class="btn-floating btn mobile-fab-tip no-wrap">${locale.getString("title.transaction.new.from.template")}</a> </li> <li> <a href="<@s.url '/transactions/newTransaction/transfer'/>" class="btn-floating btn budgetmaster-dark-green"><i class="material-icons">swap_horiz</i></a> diff --git a/src/main/resources/templates/whatsNewModal.ftl b/src/main/resources/templates/whatsNewModal.ftl new file mode 100644 index 0000000000000000000000000000000000000000..58165427c30c4586fefcc061e600e177fe609106 --- /dev/null +++ b/src/main/resources/templates/whatsNewModal.ftl @@ -0,0 +1,40 @@ +<#global locale = static["de.thecodelabs.utils.util.Localization"]> +<#import "/spring.ftl" as s> + +<div id="modalWhatsNew" class="modal modal-fixed-footer background-color"> + <div class="modal-content"> + <div class="row"> + <div class="col s12"> + <h3>${locale.getString("about.version.whatsnew")} in v${build.getVersionName()}</h3> + </div> + </div> + + <#list newsEntries as entry> + <div class="row"> + <div class="col s12"> + <h5>${entry.getHeadline()}</h5> + ${entry.getDescription()} + </div> + </div> + </#list> + + <div class="row"> + <div class="col s12"> + <h5>${locale.getString("news.further.information")}</h5> + <div> + ${locale.getString("about.date")} ${build.getVersionDate()} + </div> + <div> + ${locale.getString("news.all.releases")} <a href="${locale.getString("roadmap.url")}">${locale.getString("about.roadmap.link")}</a> + </div> + <div> + ${locale.getString("news.detailed")} <a href="https://github.com/deadlocker8/BudgetMaster/releases/tag/v${build.getVersionName()}">GitHub</a> + </div> + </div> + </div> + </div> + <div class="modal-footer background-color"> + <a id="buttonCloseWhatsNew" href="<@s.url '/about/whatsNewModal/close'/>" class="modal-action modal-close waves-effect waves-light green btn-flat white-text">${locale.getString("ok")}</a> + </div> +</div> + diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/DateRepairTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/DateRepairTest.java index 74a1d91d69d1ac27d7b2c6ea2ec2282f1cecdd4d..b665d072c97fa9092d98c3c33726d19a631238b5 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/integration/DateRepairTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/DateRepairTest.java @@ -1,6 +1,7 @@ package de.deadlocker8.budgetmaster.integration; import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; import de.deadlocker8.budgetmaster.tags.Tag; import de.deadlocker8.budgetmaster.transactions.Transaction; import de.deadlocker8.budgetmaster.transactions.TransactionRepository; @@ -21,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.sql.DataSource; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -30,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Main.class) @Import(DateRepairTest.TestDatabaseConfiguration.class) @ActiveProfiles("test") +@SeleniumTest @Transactional public class DateRepairTest { @@ -56,20 +59,20 @@ public class DateRepairTest public void test_Repeating_WithTags() { final List<Transaction> transactions = transactionRepository.findAll(); - assertThat(transactions).hasSize(4); + assertThat(transactions).hasSize(8); assertThat(transactions.stream() .map(t -> t.getTags().stream() .map(Tag::getName).toArray(String[]::new)) - .toArray(String[][]::new)) - .containsOnly(new String[]{"0815", "abc"}); + .collect(Collectors.toList())) + .containsOnly(new String[]{"0815", "abc"}, new String[0]); } @Test public void test_Repeating() { final List<Transaction> transactions = transactionRepository.findAll(); - assertThat(transactions).hasSize(4); + assertThat(transactions).hasSize(8); assertThat(transactions.stream() .map(t -> t.getDate().getHourOfDay()) diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/IntegrationTestHelper.java b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/IntegrationTestHelper.java index 80010687047adc54b9332cbe967ac49949fece6f..e4e88544458ba806e7674c1b1b15512016d6625e 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/IntegrationTestHelper.java +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/IntegrationTestHelper.java @@ -3,9 +3,14 @@ package de.deadlocker8.budgetmaster.integration.helpers; import de.thecodelabs.utils.util.Localization; import org.junit.rules.TestName; import org.openqa.selenium.*; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -66,6 +71,22 @@ public class IntegrationTestHelper } } + public void hideWhatsNewDialog() + { + try + { + WebDriverWait wait = new WebDriverWait(driver, 2); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("modalWhatsNew"))); + + WebElement buttonCloseReminder = driver.findElement(By.cssSelector("#modalWhatsNew #buttonCloseWhatsNew")); + ((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", buttonCloseReminder); + buttonCloseReminder.click(); + } + catch(NoSuchElementException | TimeoutException ignored) + { + } + } + public void uploadDatabase(String path, List<String> sourceAccounts, List<String> destinationAccounts) { if(path.startsWith("\\")) @@ -73,6 +94,15 @@ public class IntegrationTestHelper path = path.substring(1); } + try + { + path = URLDecoder.decode(path, StandardCharsets.UTF_8.toString()); + } + catch(UnsupportedEncodingException ex) + { + throw new RuntimeException(ex.getCause()); + } + driver.get(url + "/settings/database/requestImport"); // upload database @@ -92,7 +122,11 @@ public class IntegrationTestHelper matchAccounts(sourceAccounts, destinationAccounts); // confirm import - driver.findElement(By.id("buttonImport")).click(); + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("buttonImport"))); + final WebElement buttonImport = driver.findElement(By.id("buttonImport")); + buttonImport.sendKeys(""); + buttonImport.click(); assertEquals(Localization.getString("menu.settings"), IntegrationTestHelper.getTextNode(driver.findElement(By.className("headline")))); @@ -103,10 +137,14 @@ public class IntegrationTestHelper private void createAccountOnImport(String accountName) { + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("button-new-account"))); driver.findElement(By.className("button-new-account")).click(); + + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("account-name"))); WebElement inputAccountName = driver.findElement(By.id("account-name")); inputAccountName.sendKeys(accountName); - driver.findElement(By.tagName("button")).click(); + driver.findElement(By.id("button-save-account")).click(); } private void matchAccounts(List<String> sourceAccounts, List<String> destinationAccounts) @@ -125,6 +163,9 @@ public class IntegrationTestHelper WebElement sourceAccount = row.findElement(By.className("account-source")); assertEquals(sourceAccounts.get(i), IntegrationTestHelper.getTextNode(sourceAccount)); + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.className("select-dropdown"))); + row.findElement(By.className("select-dropdown")).click(); WebElement accountToSelect = row.findElement(By.xpath("//form/table/tbody/tr[" + (i + 1) + "]/td[5]/div/div/ul/li/span[text()='" + account + "']")); accountToSelect.click(); diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/TransactionTestHelper.java b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/TransactionTestHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..236fbd385f16dcfb010a418af4ccb798be021b00 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/helpers/TransactionTestHelper.java @@ -0,0 +1,59 @@ +package de.deadlocker8.budgetmaster.integration.helpers; + +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TransactionTestHelper +{ + public static void assertTransactionColumns(List<WebElement> columns, String shortDate, String categoryName, String categoryColor, boolean repeatIconVisible, boolean transferIconIsVisible, String name, String description, String amount) + { + // date + assertThat(columns.get(0)).hasFieldOrPropertyWithValue("text", shortDate); + + // category + final WebElement categoryCircle = columns.get(1).findElement(By.className("category-circle")); + assertThat(categoryCircle.getCssValue("background-color")).isEqualTo(categoryColor); + categoryName = categoryName.substring(0, 1).toUpperCase(); + assertThat(categoryCircle.findElement(By.tagName("span"))).hasFieldOrPropertyWithValue("text", categoryName); + + // icon + final List<WebElement> icons = columns.get(2).findElements(By.tagName("i")); + assertThat(icons).hasSize(1); + assertThat(icons.get(0).isDisplayed()).isEqualTo(repeatIconVisible || transferIconIsVisible); + if(repeatIconVisible) + { + assertThat(icons.get(0)).hasFieldOrPropertyWithValue("text", "repeat"); + } + else if(transferIconIsVisible) + { + assertThat(icons.get(0)).hasFieldOrPropertyWithValue("text", "swap_horiz"); + } + + // name + assertThat(columns.get(3).findElement(By.className("transaction-text")).getText()) + .isEqualTo(name); + + //description + assertThat(columns.get(3).findElement(By.className("italic")).getText()) + .isEqualTo(description); + + // amount + assertThat(columns.get(4).getText()).contains(amount); + } + + public static void selectOptionFromDropdown(WebDriver driver, By selectLocator, String nameToSelect) + { + WebElement select = driver.findElement(selectLocator); + select.findElement(By.className("select-dropdown")).click(); + + WebElement itemToSelect = select.findElement(By.xpath(".//ul/li/span[text()='" + nameToSelect + "']")); + ((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", itemToSelect); + itemToSelect.click(); + } +} diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java new file mode 100644 index 0000000000000000000000000000000000000000..82102cf6bd5dbb491ff8cbd680fce152776a239a --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/AccountTest.java @@ -0,0 +1,290 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class AccountTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, AccountTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + + String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); + helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); + } + + @Test + public void test_newAccount_cancel() + { + driver.get(helper.getUrl() + "/accounts"); + driver.findElement(By.id("button-new-account")).click(); + + // click cancel button + driver.findElement(By.xpath("//a[contains(text(),'Cancel')]")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Accounts")); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/accounts"); + + List<WebElement> accountRows = driver.findElements(By.cssSelector(".account-container tr")); + assertThat(accountRows).hasSize(3); + } + + @Test + public void test_newAccount() + { + driver.get(helper.getUrl() + "/accounts"); + driver.findElement(By.id("button-new-account")).click(); + + String name = "My new account"; + + // fill form + driver.findElement(By.id("account-name")).sendKeys(name); + + // submit form + driver.findElement(By.id("button-save-account")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Accounts")); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/accounts"); + + List<WebElement> accountRows = driver.findElements(By.cssSelector(".account-container tr")); + assertThat(accountRows).hasSize(4); + + assertAccountColumns(accountRows.get(0).findElements(By.tagName("td")), true, false, true, false, "Account2"); + assertAccountColumns(accountRows.get(1).findElements(By.tagName("td")), true, true, false, false, "Default Account"); + assertAccountColumns(accountRows.get(2).findElements(By.tagName("td")), true, false, true, false, "DefaultAccount0815"); + assertAccountColumns(accountRows.get(3).findElements(By.tagName("td")), true, false, true, false, name); + } + + @Test + public void test_edit() + { + driver.get(helper.getUrl() + "/accounts/2/edit"); + + assertThat(driver.findElement(By.id("account-name")).getAttribute("value")).isEqualTo("Default Account"); + } + + @Test + public void test_setAsDefault() + { + driver.get(helper.getUrl() + "/accounts"); + + List<WebElement> accountRows = driver.findElements(By.cssSelector(".account-container tr")); + final List<WebElement> columns = accountRows.get(0).findElements(By.tagName("td")); + final List<WebElement> icons = columns.get(0).findElements(By.tagName("i")); + + icons.get(0).click(); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/accounts"); + + accountRows = driver.findElements(By.cssSelector(".account-container tr")); + assertThat(accountRows).hasSize(3); + + assertAccountColumns(accountRows.get(0).findElements(By.tagName("td")), true, true, false, false, "Account2"); + assertAccountColumns(accountRows.get(1).findElements(By.tagName("td")), true, false, true, false, "Default Account"); + assertAccountColumns(accountRows.get(2).findElements(By.tagName("td")), true, false, true, false, "DefaultAccount0815"); + } + + @Test + public void test_setReadOnly() + { + setAsReadOnly(); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/accounts"); + + List<WebElement> accountRows = driver.findElements(By.cssSelector(".account-container tr")); + assertThat(accountRows).hasSize(3); + + assertAccountColumns(accountRows.get(0).findElements(By.tagName("td")), false, false, true, true, "Account2"); + assertAccountColumns(accountRows.get(1).findElements(By.tagName("td")), true, true, false, false, "Default Account"); + assertAccountColumns(accountRows.get(2).findElements(By.tagName("td")), true, false, true, false, "DefaultAccount0815"); + } + + private void setAsReadOnly() + { + driver.get(helper.getUrl() + "/accounts"); + + List<WebElement> accountRows = driver.findElements(By.cssSelector(".account-container tr")); + final List<WebElement> columns = accountRows.get(0).findElements(By.tagName("td")); + final List<WebElement> icons = columns.get(0).findElements(By.tagName("i")); + + icons.get(1).click(); + } + + @Test + public void test_readOnly_newTransaction_listOnlyReadableAccounts() + { + setAsReadOnly(); + + driver.get(helper.getUrl() + "/transactions"); + driver.findElement(By.id("button-new-transaction")).click(); + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transaction')]")).click(); + + // assert + WebElement select = driver.findElement(By.id("accountWrapper")); + select.findElement(By.className("select-dropdown")).click(); + + List<WebElement> items = select.findElements(By.xpath(".//ul/li/span")); + List<String> itemNames = items.stream() + .map(WebElement::getText) + .collect(Collectors.toList()); + assertThat(itemNames).containsExactly("Default Account", "DefaultAccount0815"); + } + + @Test + public void test_readOnly_preventTransactionDeleteAndEdit() + { + // select "Account2" + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("selectWrapper"), "Account2"); + + // open new transaction page + driver.get(helper.getUrl() + "/transactions"); + driver.findElement(By.id("button-new-transaction")).click(); + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transaction')]")).click(); + + // fill form + driver.findElement(By.id("transaction-name")).sendKeys("My transaction"); + driver.findElement(By.id("transaction-amount")).sendKeys("15.00"); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), "sdfdsf"); + + // submit form + driver.findElement(By.id("button-save-transaction")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // set used account as readonly + setAsReadOnly(); + + driver.get(helper.getUrl() + "/transactions"); + + // assert + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSize(2); + + final WebElement row = transactionsRows.get(0); + final List<WebElement> columns = row.findElements(By.className("col")); + + // check columns + final List<WebElement> icons = columns.get(5).findElements(By.tagName("i")); + assertThat(icons).isEmpty(); + } + + public static void assertAccountColumns(List<WebElement> columns, boolean isDefaultIconVisible, boolean isDefaultIconSelected, boolean isReadOnlyIconVisible, boolean isReadOnlyIconSelected, String name) + { + // icons + final List<WebElement> icons = columns.get(0).findElements(By.tagName("i")); + int numberOfVisibleIcons = 0; + + if(isDefaultIconVisible) + { + final WebElement icon = icons.get(numberOfVisibleIcons); + assertThat(icon.isDisplayed()).isTrue(); + if(isDefaultIconSelected) + { + assertThat(icon).hasFieldOrPropertyWithValue("text", "star"); + } + else + { + assertThat(icon).hasFieldOrPropertyWithValue("text", "star_border"); + } + + numberOfVisibleIcons++; + } + + if(isReadOnlyIconVisible) + { + final WebElement icon = icons.get(numberOfVisibleIcons); + assertThat(icon.isDisplayed()).isTrue(); + if(isReadOnlyIconSelected) + { + assertThat(icon.getAttribute("class")).contains("fa-lock"); + } + else + { + assertThat(icon.getAttribute("class")).contains("fa-lock-open"); + } + + numberOfVisibleIcons++; + } + + assertThat(icons).hasSize(numberOfVisibleIcons); + + // name + assertThat(columns.get(1)).hasFieldOrPropertyWithValue("text", name); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..66b36ee212014ee783cfc7a0432b3edf5dc3b209 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ChangeTransactionTypeTest.java @@ -0,0 +1,212 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class ChangeTransactionTypeTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, ChangeTransactionTypeTest.class); + } + }; + + private void openTransferTypeModal(int transactionID) + { + driver.get(helper.getUrl() + "/transactions/" + transactionID + "/edit"); + + driver.findElement(By.id("transaction-actions-button")).click(); + + By changeTypeButtonSelector = By.xpath("//a[contains(@data-action-type, 'changeType')][1]"); + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(changeTypeButtonSelector)); + + WebElement buttonChangeType = driver.findElement(changeTypeButtonSelector); + assertThat(buttonChangeType.isDisplayed()).isTrue(); + + buttonChangeType.click(); + assertThat(driver.findElement(By.id("modalChangeTransactionType")).isDisplayed()).isTrue(); + } + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + + String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); + helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); + } + + @Test + public void test_availableOptions_normal() + { + openTransferTypeModal(2); + + final List<WebElement> typeOptions = driver.findElements(By.cssSelector("#newTypeSelect option")); + assertThat(typeOptions).hasSize(2); + assertThat(typeOptions.get(0).getAttribute("text")).isEqualTo("Recurring"); + assertThat(typeOptions.get(1).getAttribute("text")).isEqualTo("Transfer"); + } + + @Test + public void test_availableOptions_recurring() + { + openTransferTypeModal(6); + + final List<WebElement> typeOptions = driver.findElements(By.cssSelector("#newTypeSelect option")); + assertThat(typeOptions).hasSize(2); + assertThat(typeOptions.get(0).getAttribute("text")).isEqualTo("Transaction"); + assertThat(typeOptions.get(1).getAttribute("text")).isEqualTo("Transfer"); + } + + @Test + public void test_availableOptions_transfer() + { + openTransferTypeModal(3); + + final List<WebElement> typeOptions = driver.findElements(By.cssSelector("#newTypeSelect option")); + assertThat(typeOptions).hasSize(2); + assertThat(typeOptions.get(0).getAttribute("text")).isEqualTo("Transaction"); + assertThat(typeOptions.get(1).getAttribute("text")).isEqualTo("Recurring"); + } + + @Test + public void test_normal_to_transfer() + { + openTransferTypeModal(2); + TransactionTestHelper.selectOptionFromDropdown(driver, By.cssSelector("#modalChangeTransactionType .select-wrapper"), "Transfer"); + driver.findElement(By.id("buttonChangeTransactionType")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBe(By.cssSelector(".headline"), "Edit Transfer")); + + assertThatThrownBy(()->driver.findElement(By.className("buttonExpenditure"))).isInstanceOf(NoSuchElementException.class); + + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("Test"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("15.00"); + assertThat(driver.findElement(By.id("transaction-datepicker")).getAttribute("value")).isEqualTo("01.05.2019"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEqualTo("Lorem Ipsum"); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("4"); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "123\nclose"); + + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); + assertThat(driver.findElement(By.id("transaction-transfer-account")).getAttribute("value")).isEqualTo("2"); + } + + @Test + public void test_recurring_to_normal() + { + openTransferTypeModal(6); + TransactionTestHelper.selectOptionFromDropdown(driver, By.cssSelector("#modalChangeTransactionType .select-wrapper"), "Transaction"); + driver.findElement(By.id("buttonChangeTransactionType")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBe(By.cssSelector(".headline"), "Edit Transaction")); + + assertThat(driver.findElement(By.className("buttonExpenditure")).getAttribute("class")).contains("budgetmaster-red"); + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("beste"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("15.00"); + assertThat(driver.findElement(By.id("transaction-datepicker")).getAttribute("value")).isEqualTo("01.05.2019"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEqualTo("Lorem Ipsum"); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("3"); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "123\nclose"); + + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); + } + + @Test + public void test_transfer_to_recurring() + { + openTransferTypeModal(3); + TransactionTestHelper.selectOptionFromDropdown(driver, By.cssSelector("#modalChangeTransactionType .select-wrapper"), "Recurring"); + driver.findElement(By.id("buttonChangeTransactionType")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBe(By.cssSelector(".headline"), "Edit Recurring Transaction")); + + assertThat(driver.findElement(By.className("buttonExpenditure")).getAttribute("class")).contains("budgetmaster-red"); + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("Transfer dings"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("3.00"); + assertThat(driver.findElement(By.id("transaction-datepicker")).getAttribute("value")).isEqualTo("01.05.2019"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEmpty(); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("1"); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "123\nclose"); + + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); + + assertThat(driver.findElement(By.id("transaction-repeating-modifier")).getAttribute("value")).isEmpty(); + assertThat(driver.findElement(By.id("transaction-repeating-modifier-type")).getAttribute("value")).isEqualTo("Months"); + + assertThat(driver.findElement(By.id("repeating-end-never")).isSelected()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c67ef7f9d64a5e39138d77071896795f5d6abe7e --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/FirstUseTest.java @@ -0,0 +1,105 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class FirstUseTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, FirstUseTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + } + + @Test + public void test_firstUserBanner() + { + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("firstUseBanner"))); + assertThat(driver.findElement(By.id("firstUseBanner")).isDisplayed()).isTrue(); + } + + @Test + public void test_firstUserBanner_dismiss() + { + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("firstUseBanner"))); + + driver.findElements(By.className("home-firstUseBanner-clear")).get(0).click(); + + wait.until(ExpectedConditions.invisibilityOfElementLocated(By.id("firstUseBanner"))); + assertThat(driver.findElements(By.id("firstUseBanner"))).isEmpty(); + } + + @Test + public void test_firstUserBanner_click() + { + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("firstUseBanner"))); + + driver.findElements(By.className("home-firstUseBanner")).get(0).click(); + + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "First use guide")); + + assertThat(driver.getCurrentUrl()).endsWith("/firstUse"); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1c0b590b796ae60ff129dcc6a0aa1ff9e0c35cf3 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/HotkeyTest.java @@ -0,0 +1,177 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.interactions.Action; +import org.openqa.selenium.interactions.Actions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class HotkeyTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, HotkeyTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + + String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); + helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); + } + + @Test + public void hotkey_newTransaction_normal() + { + driver.findElement(By.tagName("body")).sendKeys("n"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("form[name='NewTransaction']"))); + + assertThat(driver.getCurrentUrl()).endsWith("/newTransaction/normal"); + } + + @Test + public void hotkey_newTransaction_recurring() + { + driver.findElement(By.tagName("body")).sendKeys("r"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("form[name='NewTransaction']"))); + + assertThat(driver.getCurrentUrl()).endsWith("/newTransaction/repeating"); + } + + @Test + public void hotkey_newTransaction_transfer() + { + driver.findElement(By.tagName("body")).sendKeys("t"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("form[name='NewTransaction']"))); + + assertThat(driver.getCurrentUrl()).endsWith("/newTransaction/transfer"); + } + + @Test + public void hotkey_newTransaction_transactionFromTemplate() + { + driver.findElement(By.tagName("body")).sendKeys("v"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.id("searchTemplate"))); + + assertThat(driver.getCurrentUrl()).endsWith("/templates"); + } + + @Test + public void hotkey_filter() + { + driver.findElement(By.tagName("body")).sendKeys("f"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + assertThat(driver.getCurrentUrl()).endsWith("/transactions#modalFilter"); + assertThat(driver.findElement(By.id("modalFilter")).isDisplayed()).isTrue(); + } + + @Test + public void hotkey_search() + { + driver.findElement(By.tagName("body")).sendKeys("s"); + + assertThat(driver.findElement(By.id("search"))).isEqualTo(driver.switchTo().activeElement()); + } + + @Test + public void hotkey_saveTransaction() + { + // open transactions page + driver.get(helper.getUrl() + "/transactions/newTransaction/normal"); + + // fill mandatory inputs + driver.findElement(By.id("transaction-name")).sendKeys("My Transaction"); + driver.findElement(By.id("transaction-amount")).sendKeys("15.00"); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), "sdfdsf"); + + WebElement categoryWrapper = driver.findElement(By.id("categoryWrapper")); + Action seriesOfActions = new Actions(driver) + .keyDown(categoryWrapper, Keys.CONTROL) + .sendKeys(categoryWrapper, Keys.ENTER) + .keyUp(categoryWrapper, Keys.CONTROL) + .build(); + seriesOfActions.perform(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/transactions"); + + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSize(2); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/ImportTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java similarity index 95% rename from src/test/java/de/deadlocker8/budgetmaster/integration/ImportTest.java rename to src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java index b672db769dc95e3d573897b91b5c73729ec8c165..8dae2de6c51fe83d91db2968e78156b08a40539a 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/integration/ImportTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/ImportTest.java @@ -1,4 +1,4 @@ -package de.deadlocker8.budgetmaster.integration; +package de.deadlocker8.budgetmaster.integration.selenium; import de.deadlocker8.budgetmaster.Main; import de.deadlocker8.budgetmaster.authentication.UserService; @@ -57,7 +57,7 @@ public class ImportTest public void prepare() { FirefoxOptions options = new FirefoxOptions(); - options.setHeadless(true); + options.setHeadless(false); driver = new FirefoxDriver(options); } @Test @@ -67,6 +67,7 @@ public class ImportTest helper.start(); helper.login(UserService.DEFAULT_PASSWORD); helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); List<String> sourceAccounts = Arrays.asList("DefaultAccount0815", "sfsdf"); diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/LoginControllerTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java similarity index 96% rename from src/test/java/de/deadlocker8/budgetmaster/integration/LoginControllerTest.java rename to src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java index 93a182888cd22959936e0a44369b3848528aaef8..042553d955f7c8855bc406590cb9ea65d911fce8 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/integration/LoginControllerTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/LoginControllerTest.java @@ -1,4 +1,4 @@ -package de.deadlocker8.budgetmaster.integration; +package de.deadlocker8.budgetmaster.integration.selenium; import de.deadlocker8.budgetmaster.Main; import de.deadlocker8.budgetmaster.authentication.UserService; @@ -60,7 +60,7 @@ public class LoginControllerTest public void prepare() { FirefoxOptions options = new FirefoxOptions(); - options.setHeadless(true); + options.setHeadless(false); driver = new FirefoxDriver(options); } @@ -98,6 +98,7 @@ public class LoginControllerTest helper.start(); helper.login(UserService.DEFAULT_PASSWORD); helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); WebElement label = driver.findElement(By.id("logo-home")); String expected = helper.getUrl() + "/images/Logo_with_text_medium_res.png"; @@ -111,6 +112,7 @@ public class LoginControllerTest helper.start(); helper.login(UserService.DEFAULT_PASSWORD); helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); WebElement buttonLogout = driver.findElement(By.xpath("//body/ul/li/a[contains(text(), 'Logout')]")); JavascriptExecutor js = (JavascriptExecutor)driver; diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java new file mode 100644 index 0000000000000000000000000000000000000000..014f630c9c2c52371bcce7f9a00fcaa001f7a2ad --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionFromTemplateTest.java @@ -0,0 +1,307 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.thecodelabs.utils.util.Localization; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class NewTransactionFromTemplateTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, NewTransactionFromTemplateTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + + String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); + helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); + + // open transactions page + driver.get(helper.getUrl() + "/transactions"); + driver.findElement(By.id("button-new-transaction")).click(); + } + + @Test + public void test_newTransactionFromTemplate_FullTemplate() + { + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'From template')]")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + driver.findElements(By.cssSelector(".template-item .btn-flat no-padding text-color")); + driver.findElement(By.xpath("//li[contains(@class, 'template-item')]//a[contains(@href, '/templates/2/select')]")).click(); + + wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "New Transaction")); + + // assert + assertThat(driver.findElement(By.className("buttonExpenditure")).getAttribute("class")).contains("budgetmaster-red"); + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("NameFromTemplate"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("15.00"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEqualTo("DescriptionFromTemplate"); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("1"); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "TagFromTemplate\nclose"); + + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); + } + + @Test + public void test_newTransactionFromTemplate_OnlyIncome() + { + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'From template')]")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + driver.findElements(By.cssSelector(".template-item .btn-flat no-padding text-color")); + driver.findElement(By.xpath("//li[contains(@class, 'template-item')]//a[contains(@href, '/templates/1/select')]")).click(); + + wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "New Transaction")); + + // assert + assertThat(driver.findElement(By.className("buttonIncome")).getAttribute("class")).contains("budgetmaster-green"); + } + + @Test + public void test_selectTemplateHotkeys_initialSelect() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + final List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + + // assert + assertThat(templateItemHeaders).hasSize(2); + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_keyDown() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + final List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_DOWN); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).doesNotContain("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).contains("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_keyDown_goBackToTopFromLastItem() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + final List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_DOWN); + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_DOWN); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_keyUp_goBackToBottomFromFirstItem() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + final List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_UP); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).doesNotContain("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).contains("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_keyUp() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + final List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_UP); + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_UP); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_confirmSelection() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + final List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).doesNotContain("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ENTER); + + wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "New Transaction")); + } + + @Test + public void test_selectTemplateHotkeys_searchContainsSelection() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_DOWN); + + List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + assertThat(templateItemHeaders.get(0).getAttribute("class")).doesNotContain("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).contains("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys("emp"); + + // assert + templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + assertThat(templateItemHeaders).hasSize(2); + assertThat(templateItemHeaders.get(0).getAttribute("class")).doesNotContain("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).contains("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_searchNotContainsSelection() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + driver.findElement(By.id("searchTemplate")).sendKeys(Keys.ARROW_DOWN); + + List<WebElement> templateItemHeaders = driver.findElements(By.cssSelector(".template-item .collapsible-header")); + assertThat(templateItemHeaders.get(0).getAttribute("class")).doesNotContain("template-selected"); + assertThat(templateItemHeaders.get(1).getAttribute("class")).contains("template-selected"); + + driver.findElement(By.id("searchTemplate")).sendKeys("Income"); + + // assert + templateItemHeaders = driver.findElements(By.cssSelector(".template-item:not(.hidden) .collapsible-header")); + assertThat(templateItemHeaders).hasSize(1); + assertThat(templateItemHeaders.get(0).getAttribute("class")).contains("template-selected"); + } + + @Test + public void test_selectTemplateHotkeys_dontBlockEnterInGlobalSearch() + { + driver.get(helper.getUrl() + "/templates"); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), "Templates")); + + WebElement inputSearch = driver.findElement(By.id("search")); + inputSearch.sendKeys("e"); + inputSearch.sendKeys(Keys.ENTER); + + wait = new WebDriverWait(driver, 5); + String expected = Localization.getString("menu.search.results", 24); + wait.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector(".headline"), expected)); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/NewTransactionTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java similarity index 73% rename from src/test/java/de/deadlocker8/budgetmaster/integration/NewTransactionTest.java rename to src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java index e9c89dc78096dba079a7f5ac998b98c717229f87..c91e7b6d5406ba1816eaeb1f2d2a9c2b9edcffb2 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/integration/NewTransactionTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionNormalTest.java @@ -1,9 +1,10 @@ -package de.deadlocker8.budgetmaster.integration; +package de.deadlocker8.budgetmaster.integration.selenium; import de.deadlocker8.budgetmaster.Main; import de.deadlocker8.budgetmaster.authentication.UserService; import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -35,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) @SeleniumTest -public class NewTransactionTest +public class NewTransactionNormalTest { private IntegrationTestHelper helper; private WebDriver driver; @@ -58,7 +59,7 @@ public class NewTransactionTest @Override protected void failed(Throwable e, Description description) { - IntegrationTestHelper.saveScreenshots(driver, name, NewTransactionTest.class); + IntegrationTestHelper.saveScreenshots(driver, name, NewTransactionNormalTest.class); } }; @@ -66,7 +67,7 @@ public class NewTransactionTest public void prepare() { FirefoxOptions options = new FirefoxOptions(); - options.setHeadless(true); + options.setHeadless(false); driver = new FirefoxDriver(options); // prepare @@ -74,6 +75,7 @@ public class NewTransactionTest helper.start(); helper.login(UserService.DEFAULT_PASSWORD); helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); @@ -84,7 +86,7 @@ public class NewTransactionTest } @Test - public void newTransaction_normal_cancel() + public void test_newTransaction_cancel() { // open new transaction page driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transaction')]")).click(); @@ -103,7 +105,7 @@ public class NewTransactionTest } @Test - public void newTransaction_normal_income() + public void test_newTransaction_income() { // open new transaction page driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transaction')]")).click(); @@ -111,15 +113,17 @@ public class NewTransactionTest String name = "My normal transaction"; String amount = "15.00"; String description = "Lorem Ipsum dolor sit amet"; + String categoryName = "sdfdsf"; // fill form driver.findElement(By.className("buttonIncome")).click(); driver.findElement(By.id("transaction-name")).sendKeys(name); driver.findElement(By.id("transaction-amount")).sendKeys(amount); driver.findElement(By.id("transaction-description")).sendKeys(description); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), categoryName); // submit form - driver.findElement(By.xpath("//button[@type='submit']")).click(); + driver.findElement(By.id("button-save-transaction")).click(); WebDriverWait wait = new WebDriverWait(driver, 5); wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); @@ -136,11 +140,11 @@ public class NewTransactionTest // check columns final String dateString = new SimpleDateFormat("dd.MM.").format(new Date()); - assertTransactionColumns(columns, dateString, "N", "rgb(255, 255, 255)", false, name, description, amount); + TransactionTestHelper.assertTransactionColumns(columns, dateString, categoryName, "rgb(46, 124, 43)", false, false, name, description, amount); } @Test - public void newTransaction_normal_expenditure() + public void test_newTransaction_expenditure() { // open new transaction page driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transaction')]")).click(); @@ -148,15 +152,17 @@ public class NewTransactionTest String name = "My normal transaction"; String amount = "15.00"; String description = "Lorem Ipsum dolor sit amet"; + String categoryName = "sdfdsf"; // fill form driver.findElement(By.className("buttonExpenditure")).click(); driver.findElement(By.id("transaction-name")).sendKeys(name); driver.findElement(By.id("transaction-amount")).sendKeys(amount); driver.findElement(By.id("transaction-description")).sendKeys(description); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), categoryName); // submit form - driver.findElement(By.xpath("//button[@type='submit']")).click(); + driver.findElement(By.id("button-save-transaction")).click(); WebDriverWait wait = new WebDriverWait(driver, 5); wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); @@ -173,33 +179,25 @@ public class NewTransactionTest // check columns final String dateString = new SimpleDateFormat("dd.MM.").format(new Date()); - assertTransactionColumns(columns, dateString, "N", "rgb(255, 255, 255)", false, name, description, "-" + amount); + TransactionTestHelper.assertTransactionColumns(columns, dateString, categoryName, "rgb(46, 124, 43)", false, false, name, description, "-" + amount); } - private void assertTransactionColumns(List<WebElement> columns, String shortDate, String categoryLetter, String categoryColor, boolean repeatIconVisible, String name, String description, String amount) + @Test + public void test_edit() { - // date - assertThat(columns.get(0)).hasFieldOrPropertyWithValue("text", shortDate); - - // category - final WebElement categoryCircle = columns.get(1).findElement(By.className("category-circle")); - assertThat(categoryCircle.getCssValue("background-color")).isEqualTo(categoryColor); - assertThat(categoryCircle.findElement(By.tagName("span"))).hasFieldOrPropertyWithValue("text", categoryLetter); - - // icon - final List<WebElement> icons = columns.get(2).findElements(By.tagName("i")); - assertThat(icons).hasSize(1); - assertThat(icons.get(0).isDisplayed()).isEqualTo(repeatIconVisible); + driver.get(helper.getUrl() + "/transactions/2/edit"); - // name - assertThat(columns.get(3).findElement(By.className("transaction-text")).getText()) - .isEqualTo(name); + assertThat(driver.findElement(By.className("buttonExpenditure")).getAttribute("class")).contains("budgetmaster-red"); + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("Test"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("15.00"); + assertThat(driver.findElement(By.id("transaction-datepicker")).getAttribute("value")).isEqualTo("01.05.2019"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEqualTo("Lorem Ipsum"); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("4"); - //description - assertThat(columns.get(3).findElement(By.className("italic")).getText()) - .isEqualTo(description); + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "123\nclose"); - // amount - assertThat(columns.get(4).getText()).contains(amount); + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); } } \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4c543266efd8f45dee8855704efd54ec23e1ea3d --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionRecurringTest.java @@ -0,0 +1,260 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class NewTransactionRecurringTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, NewTransactionRecurringTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + + String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); + helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); + + // open transactions page + driver.get(helper.getUrl() + "/transactions"); + driver.findElement(By.id("button-new-transaction")).click(); + } + + @Test + public void test_newTransaction_cancel() + { + // open new transaction page + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Recurring')]")).click(); + + // click cancel button + driver.findElement(By.xpath("//a[contains(text(),'Cancel')]")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/transactions"); + + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSize(1); + } + + @Test + public void test_newTransaction_income() + { + // open new transaction page + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Recurring')]")).click(); + + String name = "My recurring transaction"; + String amount = "15.00"; + String description = "Lorem Ipsum dolor sit amet"; + String categoryName = "sdfdsf"; + String repeatingModifier = "1"; + String repeatingModifierType = "Days"; + + // fill form + driver.findElement(By.className("buttonIncome")).click(); + driver.findElement(By.id("transaction-name")).sendKeys(name); + driver.findElement(By.id("transaction-amount")).sendKeys(amount); + driver.findElement(By.id("transaction-description")).sendKeys(description); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), categoryName); + + // fill repeating options + driver.findElement(By.id("transaction-repeating-modifier")).sendKeys(repeatingModifier); + TransactionTestHelper.selectOptionFromDropdown(driver, By.cssSelector("#transaction-repeating-modifier-row"), repeatingModifierType); + + // fill date + driver.findElement(By.id("transaction-datepicker")).click(); + List<WebElement> datePickerCells = driver.findElements(By.cssSelector(".datepicker-table td")); + for(WebElement cell : datePickerCells) + { + if(cell.getText().equals("3")) + { + cell.click(); + driver.findElement(By.cssSelector(".datepicker-done")).click(); + break; + } + } + + // submit form + WebElement submitButton = driver.findElement(By.id("button-save-transaction")); + ((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", submitButton); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.elementToBeClickable(submitButton)); + + submitButton.click(); + + wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/transactions"); + + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSizeGreaterThan(2); + + final WebElement row = transactionsRows.get(transactionsRows.size()-2); + final List<WebElement> columns = row.findElements(By.className("col")); + assertThat(columns).hasSize(6); + + // check columns + final String dateString = new SimpleDateFormat("03.MM.").format(new Date()); + TransactionTestHelper.assertTransactionColumns(columns, dateString, categoryName, "rgb(46, 124, 43)", true, false, name, description, amount); + } + + @Test + public void test_newTransaction_expenditure() + { + // open new transaction page + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Recurring')]")).click(); + + String name = "My recurring transaction"; + String amount = "15.00"; + String description = "Lorem Ipsum dolor sit amet"; + String categoryName = "sdfdsf"; + String repeatingModifier = "1"; + String repeatingModifierType = "Days"; + + // fill form + driver.findElement(By.className("buttonExpenditure")).click(); + driver.findElement(By.id("transaction-name")).sendKeys(name); + driver.findElement(By.id("transaction-amount")).sendKeys(amount); + driver.findElement(By.id("transaction-description")).sendKeys(description); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), categoryName); + + // fill repeating options + driver.findElement(By.id("transaction-repeating-modifier")).sendKeys(repeatingModifier); + TransactionTestHelper.selectOptionFromDropdown(driver, By.cssSelector("#transaction-repeating-modifier-row"), repeatingModifierType); + + // fill date + driver.findElement(By.id("transaction-datepicker")).click(); + List<WebElement> datePickerCells = driver.findElements(By.cssSelector(".datepicker-table td")); + for(WebElement cell : datePickerCells) + { + if(cell.getText().equals("3")) + { + cell.click(); + driver.findElement(By.cssSelector(".datepicker-done")).click(); + break; + } + } + + // submit form + WebElement submitButton = driver.findElement(By.xpath("//button[@type='submit']")); + ((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView(true);", submitButton); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.elementToBeClickable(submitButton)); + + submitButton.click(); + + wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/transactions"); + + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSizeGreaterThan(2); + + final WebElement row = transactionsRows.get(transactionsRows.size()-2); + final List<WebElement> columns = row.findElements(By.className("col")); + assertThat(columns).hasSize(6); + + // check columns + final String dateString = new SimpleDateFormat("03.MM.").format(new Date()); + TransactionTestHelper.assertTransactionColumns(columns, dateString, categoryName, "rgb(46, 124, 43)", true, false, name, description, "-" + amount); + } + + @Test + public void test_edit() + { + driver.get(helper.getUrl() + "/transactions/6/edit"); + + assertThat(driver.findElement(By.className("buttonExpenditure")).getAttribute("class")).contains("budgetmaster-red"); + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("beste"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("15.00"); + assertThat(driver.findElement(By.id("transaction-datepicker")).getAttribute("value")).isEqualTo("01.05.2019"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEqualTo("Lorem Ipsum"); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("3"); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "123\nclose"); + + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); + + assertThat(driver.findElement(By.id("transaction-repeating-modifier")).getAttribute("value")).isEqualTo("1"); + assertThat(driver.findElement(By.id("transaction-repeating-modifier-type")).getAttribute("value")).isEqualTo("Days"); + + assertThat(driver.findElement(By.id("repeating-end-after-x-times")).isSelected()).isTrue(); + assertThat(driver.findElement(By.id("transaction-repeating-end-after-x-times-input")).getAttribute("value")).isEqualTo("20"); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java new file mode 100644 index 0000000000000000000000000000000000000000..23e6da21aaca81dfeb58cc78411a046701470d7a --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/NewTransactionTransferTest.java @@ -0,0 +1,167 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.integration.helpers.TransactionTestHelper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class NewTransactionTransferTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, NewTransactionTransferTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); + + String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); + helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); + + // open transactions page + driver.get(helper.getUrl() + "/transactions"); + driver.findElement(By.id("button-new-transaction")).click(); + } + + @Test + public void test_newTransaction_cancel() + { + // open new transaction page + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transfer')]")).click(); + + // click cancel button + driver.findElement(By.xpath("//a[contains(text(),'Cancel')]")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/transactions"); + + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSize(1); + } + + @Test + public void test_newTransaction_transfer() + { + // open new transaction page + driver.findElement(By.xpath("//div[contains(@class, 'new-transaction-button')]//a[contains(text(),'Transfer')]")).click(); + + String name = "My transfer transaction"; + String amount = "15.00"; + String description = "Lorem Ipsum dolor sit amet"; + String categoryName = "sdfdsf"; + + // fill form + driver.findElement(By.id("transaction-name")).sendKeys(name); + driver.findElement(By.id("transaction-amount")).sendKeys(amount); + driver.findElement(By.id("transaction-description")).sendKeys(description); + TransactionTestHelper.selectOptionFromDropdown(driver, By.id("categoryWrapper"), categoryName); + + // submit form + driver.findElement(By.id("button-save-transaction")).click(); + + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(".headline-date"))); + + // assert + assertThat(driver.getCurrentUrl()).endsWith("/transactions"); + + List<WebElement> transactionsRows = driver.findElements(By.cssSelector(".transaction-container .hide-on-med-and-down.transaction-row-top")); + assertThat(transactionsRows).hasSize(2); + + final WebElement row = transactionsRows.get(0); + final List<WebElement> columns = row.findElements(By.className("col")); + assertThat(columns).hasSize(6); + + // check columns + final String dateString = new SimpleDateFormat("dd.MM.").format(new Date()); + TransactionTestHelper.assertTransactionColumns(columns, dateString, categoryName, "rgb(46, 124, 43)", false, true, name, description, amount); + } + + @Test + public void test_edit() + { + driver.get(helper.getUrl() + "/transactions/3/edit"); + + assertThatThrownBy(()->driver.findElement(By.className("buttonExpenditure"))).isInstanceOf(NoSuchElementException.class); + + assertThat(driver.findElement(By.id("transaction-name")).getAttribute("value")).isEqualTo("Transfer dings"); + assertThat(driver.findElement(By.id("transaction-amount")).getAttribute("value")).isEqualTo("3.00"); + assertThat(driver.findElement(By.id("transaction-datepicker")).getAttribute("value")).isEqualTo("01.05.2019"); + assertThat(driver.findElement(By.id("transaction-description")).getAttribute("value")).isEmpty(); + assertThat(driver.findElement(By.id("transaction-category")).getAttribute("value")).isEqualTo("1"); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#transaction-chips .chip")); + assertThat(chips).hasSize(1); + assertThat(chips.get(0)).hasFieldOrPropertyWithValue("text", "123\nclose"); + + assertThat(driver.findElement(By.id("transaction-account")).getAttribute("value")).isEqualTo("3"); + assertThat(driver.findElement(By.id("transaction-transfer-account")).getAttribute("value")).isEqualTo("4"); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/SearchTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java similarity index 97% rename from src/test/java/de/deadlocker8/budgetmaster/integration/SearchTest.java rename to src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java index f44e5be4be7d77c7888ac807a59aee79e78d364a..17acb4869da5bbdd403b80e63fd6bc7e11ec3282 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/integration/SearchTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/SearchTest.java @@ -1,4 +1,4 @@ -package de.deadlocker8.budgetmaster.integration; +package de.deadlocker8.budgetmaster.integration.selenium; import de.deadlocker8.budgetmaster.Main; import de.deadlocker8.budgetmaster.authentication.UserService; @@ -65,14 +65,16 @@ public class SearchTest public void prepare() { FirefoxOptions options = new FirefoxOptions(); - options.setHeadless(true); + options.setHeadless(false); driver = new FirefoxDriver(options); + driver.manage().window().maximize(); // prepare IntegrationTestHelper helper = new IntegrationTestHelper(driver, port); helper.start(); helper.login(UserService.DEFAULT_PASSWORD); helper.hideBackupReminder(); + helper.hideWhatsNewDialog(); String path = getClass().getClassLoader().getResource("SearchDatabase.json").getFile().replace("/", File.separator); helper.uploadDatabase(path, Arrays.asList("DefaultAccount0815", "sfsdf"), Arrays.asList("DefaultAccount0815", "Account2")); diff --git a/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7d2b1314eb0ce63967e1d7f5759c72783b2863f6 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/WhatsNewTest.java @@ -0,0 +1,79 @@ +package de.deadlocker8.budgetmaster.integration.selenium; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.authentication.UserService; +import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SeleniumTest +public class WhatsNewTest +{ + private IntegrationTestHelper helper; + private WebDriver driver; + + @LocalServerPort + int port; + + @Rule + public TestName name = new TestName(); + + @Rule + public TestWatcher testWatcher = new TestWatcher() + { + @Override + protected void finished(Description description) + { + driver.quit(); + } + + @Override + protected void failed(Throwable e, Description description) + { + IntegrationTestHelper.saveScreenshots(driver, name, WhatsNewTest.class); + } + }; + + @Before + public void prepare() + { + FirefoxOptions options = new FirefoxOptions(); + options.setHeadless(false); + driver = new FirefoxDriver(options); + + // prepare + helper = new IntegrationTestHelper(driver, port); + helper.start(); + helper.login(UserService.DEFAULT_PASSWORD); + helper.hideBackupReminder(); + } + + @Test + public void test_whats_new_dialog() + { + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("modalWhatsNew"))); + assertThat(driver.findElement(By.id("modalWhatsNew")).isDisplayed()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/CategoryServiceTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/CategoryServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..be0d6e22815a5d6224571324819f38aa7609fd6f --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/CategoryServiceTest.java @@ -0,0 +1,88 @@ +package de.deadlocker8.budgetmaster.unit; + +import de.deadlocker8.budgetmaster.categories.Category; +import de.deadlocker8.budgetmaster.categories.CategoryRepository; +import de.deadlocker8.budgetmaster.categories.CategoryService; +import de.deadlocker8.budgetmaster.categories.CategoryType; +import de.deadlocker8.budgetmaster.unit.helpers.LocalizedTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@LocalizedTest +public class CategoryServiceTest +{ + private static final Category CATEGORY_NONE = new Category("No Category", "#FFFFFF", CategoryType.NONE); + private static final Category CATEGORY_REST = new Category("Rest", "#FFFF00", CategoryType.REST); + + @Mock + private CategoryRepository categoryRepository; + + @InjectMocks + private CategoryService categoryService; + + @Test + public void test_getAllCategories() + { + List<Category> categories = new ArrayList<>(); + categories.add(CATEGORY_NONE); + categories.add(CATEGORY_REST); + + Category category_BB = new Category("BB", "#ff0000", CategoryType.CUSTOM); + categories.add(category_BB); + + Category category_AA = new Category("AA", "#ff0000", CategoryType.CUSTOM); + categories.add(category_AA); + + Category category_0 = new Category("0", "#ff0000", CategoryType.CUSTOM); + categories.add(category_0); + + Category category_aa = new Category("aa", "#ff0000", CategoryType.CUSTOM); + categories.add(category_aa); + + Mockito.when(categoryRepository.findByType(CategoryType.NONE)).thenReturn(CATEGORY_NONE); + Mockito.when(categoryRepository.findByType(CategoryType.REST)).thenReturn(CATEGORY_REST); + Mockito.when(categoryRepository.findAllByOrderByNameAsc()).thenReturn(categories); + + assertThat(categoryService.getAllCategories()).hasSize(6) + .containsExactly(category_0, category_AA, category_aa, category_BB, CATEGORY_NONE, CATEGORY_REST); + } + + @Test + public void test_createDefaults() + { + categoryService.createDefaults(); + + // createDefaults() may also be called in constructor so 2 calls are possible + Mockito.verify(categoryRepository, Mockito.atLeast(1)).save(CATEGORY_NONE); + Mockito.verify(categoryRepository, Mockito.atLeast(1)).save(CATEGORY_REST); + } + + @Test + public void test_isDeletable_default() + { + Mockito.when(categoryRepository.findById(1)).thenReturn(Optional.of(CATEGORY_NONE)); + + assertThat(categoryService.isDeletable(1)).isFalse(); + } + + @Test + public void test_isDeletable_custom() + { + Category customCategory = new Category("aa", "#ff0000", CategoryType.CUSTOM); + + Mockito.when(categoryRepository.findById(1)).thenReturn(Optional.of(customCategory)); + + assertThat(categoryService.isDeletable(1)).isTrue(); + } +} diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSearchSpecificationsTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSearchSpecificationsTest.java index 18173e222d3de7229a6d0b1e994b59afa3948227..40e281d69b1f9665503a04f8423b6bf4572bd329 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSearchSpecificationsTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSearchSpecificationsTest.java @@ -213,7 +213,7 @@ public class TransactionSearchSpecificationsTest Specification spec = TransactionSearchSpecifications.withDynamicQuery(search); List<Transaction> results = transactionRepository.findAll(spec); - assertThat(results).hasSize(0); + assertThat(results).isEmpty(); } @Test @@ -223,7 +223,7 @@ public class TransactionSearchSpecificationsTest Specification spec = TransactionSearchSpecifications.withDynamicQuery(search); List<Transaction> results = transactionRepository.findAll(spec); - assertThat(results).hasSize(0); + assertThat(results).isEmpty(); } @Test diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6bbc70088de6c1f125eca15ef1125129c95a8617 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceDatabaseTest.java @@ -0,0 +1,114 @@ +package de.deadlocker8.budgetmaster.unit; + +import de.deadlocker8.budgetmaster.Main; +import de.deadlocker8.budgetmaster.accounts.AccountService; +import de.deadlocker8.budgetmaster.accounts.AccountType; +import de.deadlocker8.budgetmaster.filter.FilterConfiguration; +import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTest; +import de.deadlocker8.budgetmaster.transactions.Transaction; +import de.deadlocker8.budgetmaster.transactions.TransactionService; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.core.io.Resource; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import javax.sql.DataSource; +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Main.class) +@Import(TransactionServiceDatabaseTest.TestDatabaseConfiguration.class) +@ActiveProfiles("test") +@SeleniumTest +@Transactional +public class TransactionServiceDatabaseTest +{ + @TestConfiguration + static class TestDatabaseConfiguration + { + @Value("classpath:repeating_with_tags.mv.db") + private Resource databaseResource; + + @Bean + @Primary + public DataSource dataSource() throws IOException + { + final String folderName = databaseResource.getFile().getAbsolutePath().replace(".mv.db", ""); + String jdbcString = "jdbc:h2:/" + folderName + ";DB_CLOSE_ON_EXIT=TRUE"; + return DataSourceBuilder.create().username("sa").password("").url(jdbcString).driverClassName("org.h2.Driver").build(); + } + } + + @Autowired + private TransactionService transactionService; + + @Autowired + private AccountService accountService; + + @Test + public void test_deleteAll() + { + transactionService.deleteAll(); + + assertThat(transactionService.getRepository().findAll()).isEmpty(); + } + + @Test + public void test_getTransactionsForAccount_specificAccount() + { + DateTime date1 = DateTime.parse("2020-04-30", DateTimeFormat.forPattern("yyyy-MM-dd")); + FilterConfiguration filterConfiguration = new FilterConfiguration(true, true, true, true, true, null, null, ""); + + Transaction transaction1 = transactionService.getRepository().getOne(37); // normal transaction + Transaction transaction2 = transactionService.getRepository().getOne(9); //transfer + + List<Transaction> transactions = transactionService.getTransactionsForAccount(accountService.getRepository().findByName("Second Account"), date1, DateTime.now(), filterConfiguration); + assertThat(transactions).hasSize(2) + .containsExactlyInAnyOrder(transaction1, transaction2); + } + + @Test + public void test_getTransactionsForAccount_all() + { + DateTime date1 = DateTime.parse("2020-04-30", DateTimeFormat.forPattern("yyyy-MM-dd")); + FilterConfiguration filterConfiguration = new FilterConfiguration(true, true, true, true, true, null, null, ""); + + List<Transaction> transactions = transactionService.getTransactionsForAccount(accountService.getRepository().findAllByType(AccountType.ALL).get(0), date1, DateTime.now(), filterConfiguration); + assertThat(transactions).hasSize(7); + } + + @Test + public void test_getTransactionsForAccountUntilDate() + { + DateTime date1 = DateTime.parse("2020-04-30", DateTimeFormat.forPattern("yyyy-MM-dd")); + DateTime date2 = DateTime.parse("2020-05-20", DateTimeFormat.forPattern("yyyy-MM-dd")); + FilterConfiguration filterConfiguration = new FilterConfiguration(true, true, true, true, true, null, null, ""); + + List<Transaction> transactions = transactionService.getTransactionsForAccount(accountService.getRepository().findByName("Default Account"), date1, date2, filterConfiguration); + assertThat(transactions).hasSize(2); + } + + @Test + public void test_getTransactionsForMonthAndYear() + { + FilterConfiguration filterConfiguration = new FilterConfiguration(true, true, true, true, true, null, null, ""); + + List<Transaction> transactions = transactionService.getTransactionsForMonthAndYear(accountService.getRepository().findByName("Default Account"), 6, 2020, false, filterConfiguration); + assertThat(transactions).hasSize(1); + } +} diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c25c24357962b721ac1d930f3520bce9a8c01b8d --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionServiceTest.java @@ -0,0 +1,111 @@ +package de.deadlocker8.budgetmaster.unit; + +import de.deadlocker8.budgetmaster.accounts.Account; +import de.deadlocker8.budgetmaster.accounts.AccountType; +import de.deadlocker8.budgetmaster.categories.Category; +import de.deadlocker8.budgetmaster.categories.CategoryType; +import de.deadlocker8.budgetmaster.transactions.Transaction; +import de.deadlocker8.budgetmaster.transactions.TransactionRepository; +import de.deadlocker8.budgetmaster.transactions.TransactionService; +import de.deadlocker8.budgetmaster.unit.helpers.LocalizedTest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@LocalizedTest +public class TransactionServiceTest +{ + private static final Category CATEGORY_REST = new Category("Rest", "#FFFF00", CategoryType.REST); + private static final Category CATEGORY_CUSTOM = new Category("CustomCategory", "#0F0F0F", CategoryType.CUSTOM); + + private static final Account ACCOUNT = new Account("MyAccount", AccountType.CUSTOM); + + @Mock + private TransactionRepository transactionRepository; + + @InjectMocks + private TransactionService transactionService; + + @Test + public void test_isDeletable_rest() + { + Transaction transactionRest = new Transaction(); + transactionRest.setID(1); + transactionRest.setName("Rest"); + transactionRest.setAmount(700); + transactionRest.setCategory(CATEGORY_REST); + transactionRest.setAccount(ACCOUNT); + transactionRest.setIsExpenditure(false); + + Mockito.when(transactionRepository.findById(1)).thenReturn(Optional.of(transactionRest)); + + assertThat(transactionService.isDeletable(1)).isFalse(); + } + + @Test + public void test_isDeletable_custom() + { + Transaction transaction = new Transaction(); + transaction.setID(1); + transaction.setName("Fuel"); + transaction.setAmount(-15000); + transaction.setCategory(CATEGORY_CUSTOM); + transaction.setAccount(ACCOUNT); + transaction.setIsExpenditure(true); + + Mockito.when(transactionRepository.findById(1)).thenReturn(Optional.of(transaction)); + + assertThat(transactionService.isDeletable(1)).isTrue(); + } + + @Test + public void test_handleAmount_null() + { + Transaction transaction = new Transaction(); + transaction.setAmount(null); + transaction.setIsExpenditure(null); + + transactionService.handleAmount(transaction); + + assertThat(transaction).hasFieldOrPropertyWithValue("amount", 0) + .hasFieldOrPropertyWithValue("isExpenditure", true); + } + + @Test + public void test_handleAmount_expenditure() + { + Transaction transaction = new Transaction(); + transaction.setAmount(500); + transaction.setIsExpenditure(true); + + transactionService.handleAmount(transaction); + + assertThat(transaction).hasFieldOrPropertyWithValue("amount", -500) + .hasFieldOrPropertyWithValue("isExpenditure", true); + } + + @Test + public void test_handleAmount_income() + { + Transaction transaction = new Transaction(); + transaction.setAmount(-500); + transaction.setIsExpenditure(false); + + transactionService.handleAmount(transaction); + + assertThat(transaction).hasFieldOrPropertyWithValue("amount", 500) + .hasFieldOrPropertyWithValue("isExpenditure", false); + } +} diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSpecificationsTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSpecificationsTest.java index a834feb590237562fb35ffa75e445b45e593be54..71d28693713cfbdf2a6aaebde588e77bacecbf28 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSpecificationsTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/TransactionSpecificationsTest.java @@ -234,7 +234,7 @@ public class TransactionSpecificationsTest Specification spec = TransactionSpecifications.withDynamicQuery(startDate2019, DateTime.now(), account2, false, false, true, null, null, null, null); List<Transaction> results = transactionRepository.findAll(spec); - assertThat(results).hasSize(0); + assertThat(results).isEmpty(); } @Test @@ -267,7 +267,7 @@ public class TransactionSpecificationsTest Specification spec = TransactionSpecifications.withDynamicQuery(startDate, DateTime.now(), account, true, true, true, null, categoryIDs, null, null); List<Transaction> results = transactionRepository.findAll(spec); - assertThat(results).hasSize(0); + assertThat(results).isEmpty(); } @Test @@ -351,7 +351,7 @@ public class TransactionSpecificationsTest Specification spec = TransactionSpecifications.withDynamicQuery(startDate, DateTime.now(), account, true, true, true, null, null, tagIDs, null); List<Transaction> results = transactionRepository.findAll(spec); - assertThat(results).hasSize(0); + assertThat(results).isEmpty(); } @Test diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseImportTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseImportTest.java index 4904d6bf93be6c92e32e3222c9d361d519094753..5096be1122e75c201c3fea25b70eec2fc837dd74 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseImportTest.java +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseImportTest.java @@ -342,16 +342,22 @@ public class DatabaseImportTest transactions.add(transaction2); // templates - Template template = new Template(); - template.setTemplateName("MyTemplate"); - template.setAmount(1500); - template.setName("Transaction from Template"); + Template template1 = new Template(); + template1.setTemplateName("MyTemplate"); + template1.setAmount(1500); + template1.setAccount(sourceAccount1); + template1.setName("Transaction from Template"); List<Tag> tags2 = new ArrayList<>(); tags2.add(tag1); - template.setTags(tags2); + template1.setTags(tags2); + + Template template2 = new Template(); + template2.setTemplateName("MyTemplate2"); + template2.setTags(new ArrayList<>()); List<Template> templates = new ArrayList<>(); - templates.add(template); + templates.add(template1); + templates.add(template2); // database Database database = new Database(new ArrayList<>(), accounts, transactions, templates); @@ -384,7 +390,23 @@ public class DatabaseImportTest expectedTransaction2.setDate(new DateTime(2018, 10, 3, 12, 0, 0, 0)); expectedTransaction2.setTags(new ArrayList<>()); + Template expectedTemplate1 = new Template(); + expectedTemplate1.setTemplateName("MyTemplate"); + expectedTemplate1.setAmount(1500); + expectedTemplate1.setAccount(destAccount1); + expectedTemplate1.setName("Transaction from Template"); + List<Tag> expectedTemplateTags = new ArrayList<>(); + expectedTemplateTags.add(tag1); + expectedTemplate1.setTags(expectedTemplateTags); + + Template expectedTemplate2 = new Template(); + expectedTemplate2.setTemplateName("MyTemplate2"); + expectedTemplate2.setTags(new ArrayList<>()); + + // act + Mockito.when(tagRepository.save(Mockito.any(Tag.class))).thenReturn(tag1); + importService.importDatabase(database, accountMatchList); Database databaseResult = importService.getDatabase(); @@ -393,7 +415,7 @@ public class DatabaseImportTest .hasSize(2) .contains(expectedTransaction1, expectedTransaction2); assertThat(databaseResult.getTemplates()) - .hasSize(1) - .contains(template); + .hasSize(2) + .contains(expectedTemplate1, expectedTemplate2); } } \ No newline at end of file diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v3Test.java b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v3Test.java index f6c8f3eb54c7db452c162676a42387717037c6ab..e6aaea6a153866201098d956f8839b18bd55a84c 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v3Test.java +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v3Test.java @@ -44,7 +44,7 @@ public class DatabaseParser_v3Test @Override public String getBaseResource() { - return "languages/"; + return "languages/base"; } }); Localization.load(); diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v4Test.java b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v4Test.java index ba0d096c133aabb766ff8e164bb6fd57ed901614..f4d4084101ca9d1dc7884d6945d3eb4361ffcc02 100644 --- a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v4Test.java +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v4Test.java @@ -46,7 +46,7 @@ public class DatabaseParser_v4Test @Override public String getBaseResource() { - return "languages/"; + return "languages/base"; } }); Localization.load(); @@ -128,6 +128,7 @@ public class DatabaseParser_v4Test normalTransaction_1.setDescription("Lorem Ipsum"); normalTransaction_1.setTags(new ArrayList<>()); normalTransaction_1.setAccount(account1); + normalTransaction_1.setIsExpenditure(false); Transaction normalTransaction_2 = new Transaction(); normalTransaction_2.setAmount(-2000); @@ -136,6 +137,7 @@ public class DatabaseParser_v4Test normalTransaction_2.setDescription(""); normalTransaction_2.setAccount(account2); normalTransaction_2.setCategory(category3); + normalTransaction_2.setIsExpenditure(true); List<Tag> tags = new ArrayList<>(); Tag tag = new Tag("0815"); @@ -157,6 +159,7 @@ public class DatabaseParser_v4Test repeatingOption_1.setEndOption(new RepeatingEndAfterXTimes(2)); repeatingTransaction_1.setRepeatingOption(repeatingOption_1); repeatingTransaction_1.setTags(new ArrayList<>()); + repeatingTransaction_1.setIsExpenditure(true); Transaction repeatingTransaction_2 = new Transaction(); repeatingTransaction_2.setAmount(-12300); @@ -172,6 +175,7 @@ public class DatabaseParser_v4Test repeatingOption_2.setEndOption(new RepeatingEndAfterXTimes(2)); repeatingTransaction_2.setRepeatingOption(repeatingOption_2); repeatingTransaction_2.setTags(new ArrayList<>()); + repeatingTransaction_2.setIsExpenditure(true); Transaction transferTransaction = new Transaction(); transferTransaction.setAmount(-250); @@ -182,6 +186,7 @@ public class DatabaseParser_v4Test transferTransaction.setTransferAccount(account1); transferTransaction.setCategory(category3); transferTransaction.setTags(new ArrayList<>()); + transferTransaction.setIsExpenditure(true); assertThat(database.getTransactions()).hasSize(6) .contains(normalTransaction_1, @@ -225,6 +230,7 @@ public class DatabaseParser_v4Test normalTemplate.setDescription("Lorem Ipsum"); normalTemplate.setAccount(account1); normalTemplate.setCategory(categoryNone); + normalTemplate.setIsExpenditure(false); List<Tag> tags = new ArrayList<>(); Tag tag = new Tag("0815"); @@ -235,16 +241,18 @@ public class DatabaseParser_v4Test Template minimalTemplate = new Template(); minimalTemplate.setTemplateName("My Minimal Template"); minimalTemplate.setTags(new ArrayList<>()); + minimalTemplate.setIsExpenditure(true); Template transferTemplate = new Template(); transferTemplate.setTemplateName("My Transfer Template"); - transferTemplate.setAmount(35000); + transferTemplate.setAmount(-35000); transferTemplate.setAccount(account2); transferTemplate.setTransferAccount(account1); transferTemplate.setName("Income"); transferTemplate.setDescription("Lorem Ipsum"); transferTemplate.setCategory(category3); transferTemplate.setTags(tags); + transferTemplate.setIsExpenditure(true); assertThat(database.getTemplates()).hasSize(3) .contains(normalTemplate, minimalTemplate, transferTemplate); diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LocalizationHelpers.java b/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LocalizationHelpers.java new file mode 100644 index 0000000000000000000000000000000000000000..a78dbae012c00befff7a45a74ec0dcf2527ac595 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LocalizationHelpers.java @@ -0,0 +1,44 @@ +package de.deadlocker8.budgetmaster.unit.helpers; + +import de.thecodelabs.utils.util.Localization; +import de.thecodelabs.utils.util.localization.LocalizationMessageFormatter; +import de.thecodelabs.utils.util.localization.formatter.JavaMessageFormatter; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +import java.util.Locale; + +public class LocalizationHelpers extends AbstractTestExecutionListener +{ + @Override + public void beforeTestClass(TestContext testContext) throws Exception + { + Localization.setDelegate(new Localization.LocalizationDelegate() + { + @Override + public Locale getLocale() + { + return Locale.ENGLISH; + } + + @Override + public String[] getBaseResources() + { + return new String[]{"languages/base", "languages/news"}; + } + + @Override + public LocalizationMessageFormatter messageFormatter() + { + return new JavaMessageFormatter(); + } + + @Override + public boolean useMultipleResourceBundles() + { + return true; + } + }); + Localization.load(); + } +} diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LocalizedTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LocalizedTest.java new file mode 100644 index 0000000000000000000000000000000000000000..78cebf6993d35d49e695070544115cf3164afc78 --- /dev/null +++ b/src/test/java/de/deadlocker8/budgetmaster/unit/helpers/LocalizedTest.java @@ -0,0 +1,19 @@ +package de.deadlocker8.budgetmaster.unit.helpers; + +import org.springframework.test.context.TestExecutionListeners; + +import java.lang.annotation.*; + +import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; + +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@TestExecutionListeners( + listeners = LocalizationHelpers.class, + mergeMode = MERGE_WITH_DEFAULTS) +public @interface LocalizedTest +{ + +} diff --git a/src/test/resources/DatabaseParser_v4Test.json b/src/test/resources/DatabaseParser_v4Test.json index 87dc987c0a6d69ab3a3d01062c903b854a09bab3..045f7d329df060b639551fa21293194a1e861360 100644 --- a/src/test/resources/DatabaseParser_v4Test.json +++ b/src/test/resources/DatabaseParser_v4Test.json @@ -231,7 +231,7 @@ }, { "ID": 7, - "amount": 35000, + "amount": -35000, "account": { "ID": 3, "name": "Second Account" diff --git a/src/test/resources/SearchDatabase.json b/src/test/resources/SearchDatabase.json index 0686485205c92e6aaab9843637f3b9e0c66302d2..59c3d6cb3d24c7797648d34f1d9d6efccf43f33a 100644 --- a/src/test/resources/SearchDatabase.json +++ b/src/test/resources/SearchDatabase.json @@ -85,8 +85,13 @@ "type": "CUSTOM" }, "name": "Test", - "description": "", - "tags": [] + "description": "Lorem Ipsum", + "tags": [ + { + "ID": 1, + "name": "123" + } + ] }, { "ID": 195, @@ -105,7 +110,12 @@ }, "name": "Transfer dings", "description": "", - "tags": [], + "tags": [ + { + "ID": 1, + "name": "123" + } + ], "transferAccount": { "ID": 3, "name": "sfsdf", @@ -148,7 +158,12 @@ }, "name": "beste", "description": "Lorem Ipsum", - "tags": [], + "tags": [ + { + "ID": 1, + "name": "123" + } + ], "repeatingOption": { "ID": 161, "startDate": "2019-05-01", @@ -165,5 +180,45 @@ } } ], - "templates": [] + "templates": [ + { + "ID": 1, + "templateName": "My Income Template", + "isExpenditure": false, + "category": { + "ID": 1, + "name": "No Category", + "color": "#FFFFFF", + "type": "NONE" + }, + "name": "", + "description": "", + "tags": [] + }, + { + "ID": 2, + "templateName": "Filled Template", + "amount": -1500, + "isExpenditure": true, + "account": { + "ID": 2, + "name": "DefaultAccount0815", + "type": "CUSTOM" + }, + "category": { + "ID": 1, + "name": "No Category", + "color": "#FFFFFF", + "type": "NONE" + }, + "name": "NameFromTemplate", + "description": "DescriptionFromTemplate", + "tags": [ + { + "ID": 1, + "name": "TagFromTemplate" + } + ] + } + ] } \ No newline at end of file diff --git a/src/test/resources/repeating_with_tags.mv.db b/src/test/resources/repeating_with_tags.mv.db index c1df04f40adc4cd0acb33fb70b41bc21db44cfc2..041b43469bccd38dcbd5a0eeeb5fe38ebab13f87 100644 Binary files a/src/test/resources/repeating_with_tags.mv.db and b/src/test/resources/repeating_with_tags.mv.db differ