From e9dd70f1bcf870ef33653c8a86ebfdeb8852d945 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Mon, 7 Apr 2025 23:08:07 +0200
Subject: [PATCH] #774 - account overview: allow accounts to be filtered + save
 filter configuration in session

---
 .../accounts/AccountController.java           |  39 +++-
 .../resources/templates/accounts/accounts.ftl | 203 +++++++++---------
 2 files changed, 134 insertions(+), 108 deletions(-)

diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java
index 54b0bd55b..7e3035fae 100644
--- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java
+++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/accounts/AccountController.java
@@ -11,16 +11,16 @@ import de.deadlocker8.budgetmaster.utils.notification.Notification;
 import de.deadlocker8.budgetmaster.utils.notification.NotificationLinkBuilder;
 import de.deadlocker8.budgetmaster.utils.notification.NotificationType;
 import de.thecodelabs.utils.util.Localization;
+import jakarta.servlet.http.HttpServletRequest;
 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.*;
+import org.springframework.web.context.request.RequestAttributes;
 import org.springframework.web.context.request.WebRequest;
 
-import jakarta.servlet.http.HttpServletRequest;
-
 import java.text.MessageFormat;
 import java.time.LocalDate;
 import java.util.List;
@@ -43,6 +43,7 @@ public class AccountController extends BaseController
 		public static final String ERROR = "error";
 		public static final String NOTIFICATIONS = "notifications";
 		public static final String TODAY = "today";
+		public static final String FILTER_CONFIGURATION = "accountsFilterConfiguration";
 	}
 
 	private static class ReturnValues
@@ -115,22 +116,24 @@ public class AccountController extends BaseController
 	}
 
 	@GetMapping
-	public String accounts(Model model)
+	public String accounts(Model model, HttpServletRequest request)
 	{
-		model.addAttribute(ModelAttributes.ALL_ENTITIES, accountService.getAllEntitiesAsc());
+		final AccountsFilterConfiguration accountsFilterConfiguration = getAccountsFilterConfiguration(request);
+		model.addAttribute(ModelAttributes.ALL_ENTITIES, accountService.getFilteredEntitiesAsc(accountsFilterConfiguration));
 		return ReturnValues.SHOW_ALL;
 	}
 
 	@GetMapping("/{ID}/requestDelete")
-	public String requestDeleteAccount(Model model, @PathVariable("ID") Integer ID)
+	public String requestDeleteAccount(Model model, @PathVariable("ID") Integer ID, HttpServletRequest request)
 	{
-		model.addAttribute(ModelAttributes.ALL_ENTITIES, accountService.getAllEntitiesAsc());
+		final AccountsFilterConfiguration accountsFilterConfiguration = getAccountsFilterConfiguration(request);
+		model.addAttribute(ModelAttributes.ALL_ENTITIES, accountService.getFilteredEntitiesAsc(accountsFilterConfiguration));
 		model.addAttribute(ModelAttributes.ENTITY_TO_DELETE, accountService.getRepository().getReferenceById(ID));
 		return ReturnValues.DELETE_ENTITY;
 	}
 
 	@GetMapping("/{ID}/delete")
-	public String deleteAccountAndReferringTransactions(WebRequest request, Model model, @PathVariable("ID") Integer ID)
+	public String deleteAccountAndReferringTransactions(HttpServletRequest servletRequest, WebRequest request, Model model, @PathVariable("ID") Integer ID)
 	{
 		// at least one account is required (to delete a sole account another one has to be created first)
 		final Account accountToDelete = accountService.getRepository().getReferenceById(ID);
@@ -141,7 +144,8 @@ public class AccountController extends BaseController
 			return ReturnValues.REDIRECT_SHOW_ALL;
 		}
 
-		model.addAttribute(ModelAttributes.ALL_ENTITIES, accountService.getAllEntitiesAsc());
+		final AccountsFilterConfiguration accountsFilterConfiguration = getAccountsFilterConfiguration(servletRequest);
+		model.addAttribute(ModelAttributes.ALL_ENTITIES, accountService.getFilteredEntitiesAsc(accountsFilterConfiguration));
 		model.addAttribute(ModelAttributes.CURRENT_ACCOUNT, accountToDelete);
 		model.addAttribute(ModelAttributes.ACCOUNT_NOT_DELETABLE, true);
 		return ReturnValues.SHOW_ALL;
@@ -272,4 +276,23 @@ public class AccountController extends BaseController
 		settingsService.updateLastAccountEndDateReminderDate();
 		return "redirect:" + request.getHeader("Referer");
 	}
+
+	@PostMapping(value = "/applyFilter")
+	public String applyFilter(WebRequest request,
+							  @ModelAttribute("AccountsFilter") AccountsFilterConfiguration accountsFilterConfiguration)
+	{
+		request.setAttribute(ModelAttributes.FILTER_CONFIGURATION, accountsFilterConfiguration, RequestAttributes.SCOPE_SESSION);
+		return ReturnValues.REDIRECT_SHOW_ALL;
+	}
+
+	private AccountsFilterConfiguration getAccountsFilterConfiguration(HttpServletRequest request)
+	{
+		Object sessionAccountsFilterConfiguration = request.getSession().getAttribute(ModelAttributes.FILTER_CONFIGURATION);
+		if(sessionAccountsFilterConfiguration == null)
+		{
+			request.getSession().setAttribute(ModelAttributes.FILTER_CONFIGURATION, AccountsFilterConfiguration.DEFAULT);
+			return AccountsFilterConfiguration.DEFAULT;
+		}
+		return (AccountsFilterConfiguration) sessionAccountsFilterConfiguration;
+	}
 }
\ No newline at end of file
diff --git a/BudgetMasterServer/src/main/resources/templates/accounts/accounts.ftl b/BudgetMasterServer/src/main/resources/templates/accounts/accounts.ftl
index 10880927e..baf58c7e5 100644
--- a/BudgetMasterServer/src/main/resources/templates/accounts/accounts.ftl
+++ b/BudgetMasterServer/src/main/resources/templates/accounts/accounts.ftl
@@ -24,107 +24,110 @@
                     <div class="center-align"><@header.buttonLink url='/accounts/newAccount' icon='add' localizationKey='title.account.new' id='button-new-account'/></div>
                     <br>
                     <div class="container account-container">
-                    <table class="bordered">
-                        <thead class="hide-on-med-and-down">
-                            <tr>
-                                <th></th>
-                                <th>
-                                    <div>${locale.getString("account.new.label.state")}</div>
-                                    <div>
-                                        <label>
-                                            <input type="checkbox" />
-                                            <span><i class="fas fa-edit placeholder-icon-right"></i>${locale.getString("account.state.full.access")}</span>
-                                        </label>
-                                    </div>
-                                    <div>
-                                        <label>
-                                            <input type="checkbox" />
-                                            <span><i class="fas fa-lock placeholder-icon-right"></i>${locale.getString("account.state.read.only")}</span>
-                                        </label>
-                                    </div>
-                                    <div>
-                                        <label>
-                                            <input type="checkbox" />
-                                            <span><i class="far fa-eye-slash placeholder-icon-right"></i>${locale.getString("account.state.hidden")}</span>
-                                        </label>
-                                    </div>
-                                </th>
-                                <th>
-                                    <div>${locale.getString("account.new.label.endDate")}</div>
-                                    <div>
-                                        <label>
-                                            <input type="checkbox" />
-                                            <span><i class="fas fa-bell placeholder-icon-right"></i>${locale.getString("account.label.endDate.with")}</span>
-                                        </label>
-                                    </div>
-                                    <div>
-                                        <label>
-                                            <input type="checkbox" />
-                                            <span><i class="fas fa-bell-slash placeholder-icon-right"></i>${locale.getString("account.label.endDate.without")}</span>
-                                        </label>
-                                    </div>
-                                </th>
-                                <th>
-                                    <div>${locale.getString("account.new.label.name")}</div>
-                                    <div class="input-field">
-                                        <input type="text" id="accounts-filter-name">
-                                    </div>
-                                </th>
-                                <th>
-                                    <div>${locale.getString("transaction.new.label.description")}</div>
-                                    <div class="input-field">
-                                        <input type="text" id="accounts-filter-description">
-                                    </div>
-                                </th>
-                                <th class="vertical-align-middle">
-                                    <button class="btn waves-effect waves-light background-green" type="submit" id="accounts-filter-button">
-                                        <i class="fas fa-filter left"></i>${locale.getString("filter.apply")}
-                                    </button>
-                                </th>
-                            </tr>
-                        </thead>
+                        <form name="AccountsFilter" action="<@s.url '/accounts/applyFilter'/>" method="post">
+                            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
+                            <table class="bordered">
+                                <thead class="hide-on-med-and-down">
+                                    <tr>
+                                        <th></th>
+                                        <th>
+                                            <div>${locale.getString("account.new.label.state")}</div>
+                                            <div>
+                                                <label>
+                                                    <input type="checkbox" name="includeFullAccess" <#if accountsFilterConfiguration?? && accountsFilterConfiguration.includeFullAccess>checked</#if>/>
+                                                    <span><i class="fas fa-edit placeholder-icon-right"></i>${locale.getString("account.state.full.access")}</span>
+                                                </label>
+                                            </div>
+                                            <div>
+                                                <label>
+                                                    <input type="checkbox" name="includeReadOnly" <#if accountsFilterConfiguration?? && accountsFilterConfiguration.includeReadOnly>checked</#if>/>
+                                                    <span><i class="fas fa-lock placeholder-icon-right"></i>${locale.getString("account.state.read.only")}</span>
+                                                </label>
+                                            </div>
+                                            <div>
+                                                <label>
+                                                    <input type="checkbox" name="includeHidden" <#if accountsFilterConfiguration?? && accountsFilterConfiguration.includeHidden>checked</#if>/>
+                                                    <span><i class="far fa-eye-slash placeholder-icon-right"></i>${locale.getString("account.state.hidden")}</span>
+                                                </label>
+                                            </div>
+                                        </th>
+                                        <th>
+                                            <div>${locale.getString("account.new.label.endDate")}</div>
+                                            <div>
+                                                <label>
+                                                    <input type="checkbox" name="includeWithEndDate" <#if accountsFilterConfiguration?? && accountsFilterConfiguration.includeWithEndDate>checked</#if>/>
+                                                    <span><i class="fas fa-bell placeholder-icon-right"></i>${locale.getString("account.label.endDate.with")}</span>
+                                                </label>
+                                            </div>
+                                            <div>
+                                                <label>
+                                                    <input type="checkbox" name="includeWithoutEndDate" <#if accountsFilterConfiguration?? && accountsFilterConfiguration.includeWithoutEndDate>checked</#if>/>
+                                                    <span><i class="fas fa-bell-slash placeholder-icon-right"></i>${locale.getString("account.label.endDate.without")}</span>
+                                                </label>
+                                            </div>
+                                        </th>
+                                        <th>
+                                            <div>${locale.getString("account.new.label.name")}</div>
+                                            <div class="input-field">
+                                                <input type="text" name="name" value="<#if accountsFilterConfiguration??>${accountsFilterConfiguration.name}</#if>"/>
+                                            </div>
+                                        </th>
+                                        <th>
+                                            <div>${locale.getString("transaction.new.label.description")}</div>
+                                            <div class="input-field">
+                                                <input type="text" name="description" value="<#if accountsFilterConfiguration??>${accountsFilterConfiguration.description}</#if>"/>
+                                            </div>
+                                        </th>
+                                        <th class="vertical-align-middle">
+                                            <button class="btn waves-effect waves-light background-green" type="submit" id="accounts-filter-button">
+                                                <i class="fas fa-filter left"></i>${locale.getString("filter.apply")}
+                                            </button>
+                                        </th>
+                                    </tr>
+                                </thead>
 
-                        <#list accounts as account>
-                            <#if (account.getType().name() == "CUSTOM")>
-                                <tr class="account-overview-row">
-                                    <td><@customSelectMacros.accountIcon account account.getName() "text-blue"/></td>
-                                    <td>
-                                        <#if account.getAccountState().name() == "READ_ONLY">
-                                            <div class="placeholder-icon placeholder-icon-right"></div>
-                                            <i class="fas fa-lock placeholder-icon-right"></i>
-                                        <#elseif account.getAccountState().name() == "HIDDEN">
-                                            <div class="placeholder-icon placeholder-icon-right"></div>
-                                            <i class="far fa-eye-slash placeholder-icon-right"></i>
-                                        <#else>
-                                            <a href="<@s.url '/accounts/${account.getID()?c}/setAsDefault'/>" class="btn-flat no-padding text-default 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>
-                                            <i class="fas fa-edit placeholder-icon-right"></i>
-                                        </#if>
-                                    </td>
-                                    <td>
-                                        <#if account.getEndDate()??>
-                                            <i class="fas fa-bell placeholder-icon-right"></i> ${dateService.getDateStringNormal(account.getEndDate())}
-                                        <#else>
-                                            <div class="placeholder-icon"></div>
-                                        </#if>
-                                    </td>
-                                    <td>${account.getName()}</td>
-                                    <td>
-                                        <div class="truncate account-description">
-                                            ${account.getDescription()}
-                                        </div>
-                                    </td>
-                                    <td>
-                                        <a href="<@s.url '/accounts/${account.getID()?c}/edit'/>" class="btn-flat no-padding text-default"><i class="material-icons left">edit</i></a>
-                                        <@header.buttonFlat url='/accounts/' + account.ID?c + '/requestDelete' icon='delete' localizationKey='' classes="no-padding text-default button-request-delete-account" isDataUrl=true/>
-                                    </td>
-                                </tr>
-                            </#if>
-                        </#list>
-                    </table>
-                    <#if accounts?size == 0>
-                        <div class="headline center-align">${locale.getString("placeholder")}</div>
-                    </#if>
-                </div>
+                                <#list accounts as account>
+                                    <#if (account.getType().name() == "CUSTOM")>
+                                        <tr class="account-overview-row">
+                                            <td><@customSelectMacros.accountIcon account account.getName() "text-blue"/></td>
+                                            <td>
+                                                <#if account.getAccountState().name() == "READ_ONLY">
+                                                    <div class="placeholder-icon placeholder-icon-right"></div>
+                                                    <i class="fas fa-lock placeholder-icon-right"></i>
+                                                <#elseif account.getAccountState().name() == "HIDDEN">
+                                                    <div class="placeholder-icon placeholder-icon-right"></div>
+                                                    <i class="far fa-eye-slash placeholder-icon-right"></i>
+                                                <#else>
+                                                    <a href="<@s.url '/accounts/${account.getID()?c}/setAsDefault'/>" class="btn-flat no-padding text-default 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>
+                                                    <i class="fas fa-edit placeholder-icon-right"></i>
+                                                </#if>
+                                            </td>
+                                            <td>
+                                                <#if account.getEndDate()??>
+                                                    <i class="fas fa-bell placeholder-icon-right"></i> ${dateService.getDateStringNormal(account.getEndDate())}
+                                                <#else>
+                                                    <div class="placeholder-icon"></div>
+                                                </#if>
+                                            </td>
+                                            <td>${account.getName()}</td>
+                                            <td>
+                                                <div class="truncate account-description">
+                                                    ${account.getDescription()}
+                                                </div>
+                                            </td>
+                                            <td>
+                                                <a href="<@s.url '/accounts/${account.getID()?c}/edit'/>" class="btn-flat no-padding text-default"><i class="material-icons left">edit</i></a>
+                                                <@header.buttonFlat url='/accounts/' + account.ID?c + '/requestDelete' icon='delete' localizationKey='' classes="no-padding text-default button-request-delete-account" isDataUrl=true/>
+                                            </td>
+                                        </tr>
+                                    </#if>
+                                </#list>
+                            </table>
+                        </form>
+                        <#if accounts?size == 0>
+                            <div class="headline center-align">${locale.getString("placeholder")}</div>
+                        </#if>
+                    </div>
                 </@header.content>
             </div>
 
-- 
GitLab