diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java index 4fe7d231e822b69001a47db121cf61bbd25c1c5e..2b0e367e99f2eb1d5e26c2b0764fcf0f21a79f6d 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java @@ -13,6 +13,9 @@ import de.deadlocker8.budgetmaster.hints.HintService; import de.deadlocker8.budgetmaster.services.ImportResultItem; import de.deadlocker8.budgetmaster.services.ImportService; import de.deadlocker8.budgetmaster.settings.containers.*; +import de.deadlocker8.budgetmaster.transactions.keywords.TransactionNameKeyword; +import de.deadlocker8.budgetmaster.transactions.keywords.TransactionNameKeywordRepository; +import de.deadlocker8.budgetmaster.transactions.keywords.TransactionNameKeywordService; import de.deadlocker8.budgetmaster.update.BudgetMasterUpdateService; import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.WebRequestUtils; @@ -65,6 +68,7 @@ public class SettingsController extends BaseController public static final String AUTO_BACKUP_STATUS = "autoBackupStatus"; public static final String NEXT_BACKUP_TIME = "nextBackupTime"; public static final String TOAST_CONTENT = "toastContent"; + public static final String TRANSACTION_NAME_KEYWORDS = "transactionNameKeywords"; } private static class ReturnValues @@ -95,11 +99,12 @@ public class SettingsController extends BaseController private final BudgetMasterUpdateService budgetMasterUpdateService; private final BackupService backupService; private final HintService hintService; + private final TransactionNameKeywordService keywordService; private final List<Integer> SEARCH_RESULTS_PER_PAGE_OPTIONS = Arrays.asList(10, 20, 25, 30, 50, 100); @Autowired - public SettingsController(SettingsService settingsService, DatabaseService databaseService, CategoryService categoryService, ImportService importService, BudgetMasterUpdateService budgetMasterUpdateService, BackupService backupService, HintService hintService) + public SettingsController(SettingsService settingsService, DatabaseService databaseService, CategoryService categoryService, ImportService importService, BudgetMasterUpdateService budgetMasterUpdateService, BackupService backupService, HintService hintService, TransactionNameKeywordService keywordService) { this.settingsService = settingsService; this.databaseService = databaseService; @@ -108,6 +113,7 @@ public class SettingsController extends BaseController this.budgetMasterUpdateService = budgetMasterUpdateService; this.backupService = backupService; this.hintService = hintService; + this.keywordService = keywordService; } @GetMapping @@ -152,6 +158,19 @@ public class SettingsController extends BaseController @ModelAttribute("TransactionsSettingsContainer") TransactionsSettingsContainer transactionsSettingsContainer, BindingResult bindingResult) { + final TransactionNameKeywordRepository keywordRepository = keywordService.getRepository(); + keywordRepository.deleteAll(); + + final List<TransactionNameKeyword> keywords = transactionsSettingsContainer.getKeywords(); + if(keywords != null) + { + for(TransactionNameKeyword keyword : keywords) + { + keyword.setID(null); + keywordRepository.save(keyword); + } + } + return saveContainer(model, transactionsSettingsContainer, bindingResult).templatePath(); } @@ -458,6 +477,7 @@ public class SettingsController extends BaseController model.addAttribute(ModelAttributes.SETTINGS, settings); model.addAttribute(ModelAttributes.SEARCH_RESULTS_PER_PAGE, SEARCH_RESULTS_PER_PAGE_OPTIONS); model.addAttribute(ModelAttributes.AUTO_BACKUP_TIME, AutoBackupTime.values()); + model.addAttribute(ModelAttributes.TRANSACTION_NAME_KEYWORDS, keywordService.getRepository().findAll()); final Optional<LocalDateTime> nextBackupTimeOptional = backupService.getNextRun(); nextBackupTimeOptional.ifPresent(date -> model.addAttribute(ModelAttributes.NEXT_BACKUP_TIME, date)); diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/TransactionsSettingsContainer.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/TransactionsSettingsContainer.java index 7af7bd174954634a54794b0d45b4f9980337072d..7ce5b0cdcb1dcfe7ce769b9a7458a3dd109e962c 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/TransactionsSettingsContainer.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/settings/containers/TransactionsSettingsContainer.java @@ -2,15 +2,22 @@ package de.deadlocker8.budgetmaster.settings.containers; import de.deadlocker8.budgetmaster.settings.Settings; import de.deadlocker8.budgetmaster.settings.SettingsService; +import de.deadlocker8.budgetmaster.transactions.keywords.TransactionNameKeyword; import org.springframework.validation.Errors; +import java.util.List; + + public final class TransactionsSettingsContainer implements SettingsContainer { private Boolean restActivated; - public TransactionsSettingsContainer(Boolean restActivated) + private List<TransactionNameKeyword> keywords; + + public TransactionsSettingsContainer(Boolean restActivated, List<TransactionNameKeyword> keywords) { this.restActivated = restActivated; + this.keywords = keywords; } @Override @@ -57,8 +64,18 @@ public final class TransactionsSettingsContainer implements SettingsContainer } @Override - public void persistChanges(SettingsService settingsService, Settings previousSettings, Settings settings) + public void persistChanges(SettingsService settingsService, Settings previousSettings, Settings settings) { settingsService.updateSettings(settings); } + + public List<TransactionNameKeyword> getKeywords() + { + return keywords; + } + + public void setKeywords(List<TransactionNameKeyword> keywords) + { + this.keywords = keywords; + } } diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties index 6e4286cfc1d6d491b04225ce614289766d36839b..83b8bcb9847c97ba2d3cca677bb89537c08a9816 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_de.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties @@ -328,6 +328,9 @@ settings.category.circle.style.activated=Kreise settings.personalization=Personalisierung settings.personalization.reload.page=Zum Anwenden visueller Änderungen <a href="">Seite neu laden</a> settings.transactions=Buchungen +settings.transactions.keywords=Schlüsselwörter +settings.transactions.keywords.placeholder=Schlüsselwort hier eingeben +settings.transactions.keywords.description=Verwendest du eins dieser Schlüsselwörter im Namen einer neuen Buchung, so wirst du daran erinnert die Buchung als Einnahme zu markieren. settings.misc=Sonstiges settings.warning.unsaved=ungespeichert diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties index 5fe07b39d5871fcae8c1735747e93b510b1f2a49..42d367cce128801e7d36cbce8b69ac6807d84059 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_en.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties @@ -329,6 +329,9 @@ settings.category.circle.style.activated=Circles settings.personalization=Personalization settings.personalization.reload.page=<a href="">Reload</a> page to apply visual changes settings.transactions=Transactions +settings.transactions.keywords=Keywords +settings.transactions.keywords.placeholder=Enter keyword here +settings.transactions.keywords.description=If you use one of these keywords in the name of a new transaction, you will be reminded to mark the transaction as an income. settings.misc=Misc settings.warning.unsaved=unsaved diff --git a/BudgetMasterServer/src/main/resources/static/css/settings.css b/BudgetMasterServer/src/main/resources/static/css/settings.css index f4f810b65ad0af926b9e67d3f1bb77353414b86a..1239a3841af9c25b10c0127d4cca7f78eabd6406 100644 --- a/BudgetMasterServer/src/main/resources/static/css/settings.css +++ b/BudgetMasterServer/src/main/resources/static/css/settings.css @@ -68,4 +68,18 @@ .settings-preview-image:hover { opacity: 1.0 !important; cursor: pointer; +} + +#settings-keywords { + margin-top: 0; +} + +#settings-keywords-description { + display: flex; + flex-direction: row; + align-items: center; +} + +#settings-keywords-description i { + margin-right: 1.3rem; } \ No newline at end of file diff --git a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl index ff1b19a44c410a1b53a4517790c3d8287e9c90a6..02992a4d6944d790f2ce623ff4c6bfb3a09f6154 100644 --- a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl +++ b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsBackup.ftl @@ -7,7 +7,7 @@ <#import "../settingsMacros.ftl" as settingsMacros> <#macro backupSettingsContainer importScripts settings> - <@settingsContainerMacros.settingsContainer 'BackupSettingsContainer' 'backupSettingsContainer' importScripts '/settings/save/backup' true> + <@settingsContainerMacros.settingsContainer 'BackupSettingsContainer' 'backupSettingsContainer' importScripts '/settings/save/backup' 'validateBackupForm()'> <div class="row"> <div class="col s12"> <div class="table-container"> @@ -50,7 +50,7 @@ $('#settings-auto-backup-git-remote').toggle(newSelectedIndex === 2); // git remote } - function validateForm() + function validateBackupForm() { let autoBackupCheckbox = document.getElementsByName("autoBackupActivated")[0]; if(autoBackupCheckbox.checked) diff --git a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl index 48d9f29bf21e4ab4d8233f5e1ce22a1caed80592..a9f7cf5d93fdc20fd0925a68b9efe89d62d089df 100644 --- a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl +++ b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsContainer.ftl @@ -1,7 +1,7 @@ <#import "/spring.ftl" as s> -<#macro settingsContainer formName containerId importScripts actionUrl validateForm=false> - <form name="${formName}" method="post" <#if validateForm>onsubmit="return validateForm()"</#if> action="<@s.url actionUrl/>"> +<#macro settingsContainer formName containerId importScripts actionUrl validateForm=''> + <form name="${formName}" method="post" <#if validateForm??>onsubmit="return ${validateForm}"</#if> action="<@s.url actionUrl/>"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" id="token"/> <#nested> diff --git a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsTransactions.ftl b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsTransactions.ftl index 03eae9c4716a25e40d83d197aeef10c1f16f2a0a..25663f6feed120337eeb14c929a1e0134808b6d7 100644 --- a/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsTransactions.ftl +++ b/BudgetMasterServer/src/main/resources/templates/settings/containers/settingsTransactions.ftl @@ -7,7 +7,7 @@ <#import "../settingsMacros.ftl" as settingsMacros> <#macro transactionsSettingsContainer importScripts settings> - <@settingsContainerMacros.settingsContainer 'TransactionsSettingsContainer' 'transactionsSettingsContainer' importScripts '/settings/save/transactions'> + <@settingsContainerMacros.settingsContainer 'TransactionsSettingsContainer' 'transactionsSettingsContainer' importScripts '/settings/save/transactions' 'validateTransactionForm()'> <div class="row"> <div class="col s12"> <div class="table-container"> @@ -28,6 +28,40 @@ </div> </div> + <div class="row no-margin-bottom"> + <div class="col s12 m12 l8 offset-l2"> + <div class="tag-input-container"> + <i class="material-icons prefix">rule_folder</i> + <div class="tag-input"> + <label class="input-label" class="chips-label" for="settings-keywords">${locale.getString("settings.transactions.keywords")}</label> + <div id="settings-keywords" class="chips chips-placeholder"></div> + </div> + </div> + </div> + <div id="hidden-transaction-name-keywords"></div> + <script> + tagsPlaceholder = "${locale.getString("settings.transactions.keywords.placeholder")}"; + var initialKeywords = [ + <#list transactionNameKeywords as keyword> + {tag: '${keyword.getValue()?replace("'", "\\'")}'}, + </#list> + ]; + </script> + </div> + + <div class="row"> + <div class="col s10 offset-s1 m8 offset-m2 l6 offset-l3"> + <div id="settings-keywords-description"> + <i class="material-icons">help_outline</i> + <div> + ${locale.getString("settings.transactions.keywords.description")} + </div> + </div> + </div> + </div> + + <br> + <div class="row"> <div class="col s12 center-align"> <@header.buttonSubmit name='action' icon='save' localizationKey='save' color='background-green'/> @@ -35,6 +69,34 @@ </div> <script> + M.Chips.init(document.querySelectorAll('.chips'), { + placeholder: tagsPlaceholder, + data: initialKeywords, + onChipAdd: onKeywordsChange, + onChipDelete: onKeywordsChange + }); + + function onKeywordsChange() + { + toggleSettingsContainerHeader('transactionsSettingsContainerHeader', false); + } + + function validateTransactionForm() + { + let keywords = M.Chips.getInstance(document.querySelector('.chips')).chipsData; + let parent = document.getElementById('hidden-transaction-name-keywords'); + for(let i = 0; i < keywords.length; i++) + { + let input = document.createElement('input'); + input.setAttribute('type', 'hidden'); + input.setAttribute('name', 'keywords[' + i + '].value'); + input.setAttribute('value', keywords[i].tag); + parent.appendChild(input); + } + + return false; + } + $('input[name="restActivated"]').change(function() { toggleSettingsContainerHeader('transactionsSettingsContainerHeader', false); diff --git a/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl b/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl index 6d9c4236856c734a3aeb5883a0e43c372942cbcf..31a949d98d2bf5eea2f0eb7907883e1b481f321a 100644 --- a/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl +++ b/BudgetMasterServer/src/main/resources/templates/settings/settings.ftl @@ -71,7 +71,7 @@ </div> </div> - <form name="Settings" action="<@s.url '/settings/save'/>" method="post" onsubmit="return validateForm()"> + <form name="Settings" action="<@s.url '/settings/save'/>" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" id="token"/> <input type="hidden" name="ID" value="${settings.getID()?c}"> <input type="hidden" name="lastBackupReminderDate" value="${dateService.getLongDateString(settings.getLastBackupReminderDate())}"> diff --git a/BudgetMasterServer/src/main/resources/templates/transactions/newTransactionMacros.ftl b/BudgetMasterServer/src/main/resources/templates/transactions/newTransactionMacros.ftl index 6d871b8912060e83ddaff98ef57bd736c4d998e8..43aae2ff4b64dcd99466377da89ffb779f99ae23 100644 --- a/BudgetMasterServer/src/main/resources/templates/transactions/newTransactionMacros.ftl +++ b/BudgetMasterServer/src/main/resources/templates/transactions/newTransactionMacros.ftl @@ -122,9 +122,9 @@ <#macro transactionTags transaction> <div class="row"> <div class="col s12 m12 l8 offset-l2"> - <div class="transaction-tags"> + <div class="tag-input-container"> <i class="material-icons prefix">local_offer</i> - <div class="transaction-tags-input"> + <div class="tag-input"> <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> diff --git a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/settings/TransactionSettingsTest.java b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/settings/TransactionSettingsTest.java index 4c5dad0ac8defff3aa4748d340bbca7c0a4cd614..f0fd89ddcf34638577b15343951fee3493a6058f 100644 --- a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/settings/TransactionSettingsTest.java +++ b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/integration/selenium/settings/TransactionSettingsTest.java @@ -5,10 +5,13 @@ import de.deadlocker8.budgetmaster.integration.helpers.IntegrationTestHelper; import de.deadlocker8.budgetmaster.integration.helpers.SeleniumTestBase; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; import java.time.Duration; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +60,11 @@ class TransactionSettingsTest extends SeleniumTestBase driver.findElement(By.cssSelector("#transactionsSettingsContainer .lever")).click(); + final WebElement keywordInput = driver.findElement(By.cssSelector("#settings-keywords input")); + keywordInput.click(); + keywordInput.sendKeys("abc"); + keywordInput.sendKeys(Keys.ENTER); + driver.findElement(By.cssSelector("#transactionsSettingsContainer button")).click(); wait = new WebDriverWait(driver, Duration.ofSeconds(5)); @@ -68,5 +76,9 @@ class TransactionSettingsTest extends SeleniumTestBase assertThat(driver.findElement(By.cssSelector("#transactionsSettingsContainerHeader .collapsible-header-button")).isDisplayed()) .isFalse(); + + final List<WebElement> chips = driver.findElements(By.cssSelector("#settings-keywords .chip")); + assertThat(chips.get(chips.size() - 1).getText()) + .contains("abc"); } } \ No newline at end of file diff --git a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/settings/containers/TransactionSettingsContainerTest.java b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/settings/containers/TransactionSettingsContainerTest.java index 951392a6b79d79814d178c244ed108e283d70b28..48a4e73d4770532b0daad0e7f43a8ed0e97a3de8 100644 --- a/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/settings/containers/TransactionSettingsContainerTest.java +++ b/BudgetMasterServer/src/test/java/de/deadlocker8/budgetmaster/unit/settings/containers/TransactionSettingsContainerTest.java @@ -12,6 +12,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; @@ -25,7 +27,7 @@ class TransactionSettingsContainerTest @Test void test_validate_valid() { - final TransactionsSettingsContainer container = new TransactionsSettingsContainer(true); + final TransactionsSettingsContainer container = new TransactionsSettingsContainer(true, List.of()); final Errors errors = new BeanPropertyBindingResult(container, "container"); container.validate(errors); @@ -37,7 +39,7 @@ class TransactionSettingsContainerTest @Test void test_fixBooleans() { - final TransactionsSettingsContainer container = new TransactionsSettingsContainer(null); + final TransactionsSettingsContainer container = new TransactionsSettingsContainer(null, List.of()); container.fixBooleans(); @@ -52,7 +54,7 @@ class TransactionSettingsContainerTest Mockito.when(settingsService.getSettings()).thenReturn(defaultSettings); - final TransactionsSettingsContainer container = new TransactionsSettingsContainer(false); + final TransactionsSettingsContainer container = new TransactionsSettingsContainer(false, List.of()); final Settings updatedSettings = container.updateSettings(settingsService); assertThat(updatedSettings)