From 56e5928e4bc5a053959b163fbe8494bd72db07aa Mon Sep 17 00:00:00 2001 From: effe Date: Sun, 15 Sep 2024 16:55:28 -0400 Subject: [PATCH] feat: expense category filtering --- .../com/application/munera/data/Category.java | 15 ++++++ .../munera/services/ViewsService.java | 20 +++++++- .../munera/views/expenses/ExpensesView.java | 49 +++++++++---------- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/application/munera/data/Category.java b/src/main/java/com/application/munera/data/Category.java index 51bb6ba..96c1d39 100644 --- a/src/main/java/com/application/munera/data/Category.java +++ b/src/main/java/com/application/munera/data/Category.java @@ -5,6 +5,8 @@ import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; +import java.util.Objects; + @Entity @Getter @Setter @@ -24,4 +26,17 @@ public class Category { @Column(name = "userId", nullable = false) private Long userId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Category category = (Category) o; + return id != null && id.equals(category.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/src/main/java/com/application/munera/services/ViewsService.java b/src/main/java/com/application/munera/services/ViewsService.java index bc375bf..653d9b4 100644 --- a/src/main/java/com/application/munera/services/ViewsService.java +++ b/src/main/java/com/application/munera/services/ViewsService.java @@ -1,8 +1,10 @@ package com.application.munera.services; import com.application.munera.data.BadgeMessage; +import com.application.munera.data.Category; import com.application.munera.data.Expense; import com.application.munera.data.ExpenseType; +import com.vaadin.flow.component.combobox.MultiSelectComboBox; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.textfield.TextField; import org.springframework.stereotype.Service; @@ -47,8 +49,8 @@ public class ViewsService { return badge; } - public void applyFilter(TextField nameFilter, Long userId, PaginatedGrid grid) { - String filterValue = nameFilter.getValue().trim(); + public void applyNameFilter(TextField nameFilter, Long userId, PaginatedGrid grid) { + final var filterValue = nameFilter.getValue().trim(); List filteredExpenses; if (filterValue.isEmpty()) filteredExpenses = expenseService.findAllOrderByDateDescending(userId); // If the filter is empty, return all expenses else { @@ -60,6 +62,20 @@ public class ViewsService { grid.setItems(filteredExpenses); } + public void applyCategoryFilter(MultiSelectComboBox categoryFilter, Long userId, PaginatedGrid grid) { + final var selectedCategories = categoryFilter.getValue(); + List filteredExpenses; + if (selectedCategories.isEmpty()) filteredExpenses = expenseService.findAllOrderByDateDescending(userId); // If no categories are selected, return all expenses + else { + // Apply the filter by selected categories + filteredExpenses = expenseService.findAllOrderByDateDescending(userId) + .stream() + .filter(expense1 -> selectedCategories.contains(expense1.getCategory())) + .toList(); + } + grid.setItems(filteredExpenses); + } + private BadgeMessage determineBadgeMessage(ExpenseType type, boolean isPaid) { return switch (type) { case CREDIT -> isPaid ? BadgeMessage.PAID_TO_ME : BadgeMessage.OWED_TO_ME; diff --git a/src/main/java/com/application/munera/views/expenses/ExpensesView.java b/src/main/java/com/application/munera/views/expenses/ExpensesView.java index 99b3126..f21d76d 100644 --- a/src/main/java/com/application/munera/views/expenses/ExpensesView.java +++ b/src/main/java/com/application/munera/views/expenses/ExpensesView.java @@ -15,6 +15,7 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.combobox.MultiSelectComboBox; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.component.formlayout.FormLayout; @@ -24,6 +25,7 @@ import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.Notification.Position; import com.vaadin.flow.component.notification.NotificationVariant; +import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.splitlayout.SplitLayout; @@ -56,6 +58,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { private final PaginatedGrid grid = new PaginatedGrid<>(); private final TextField nameFilter = new TextField(); + private final MultiSelectComboBox categoryFilter = new MultiSelectComboBox<>(); private final Button cancel = new Button("Cancel"); private final Button save = new Button("Save"); private final Button delete = new Button("Delete"); @@ -86,7 +89,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver { this.expenseService = expenseService; this.categoryService = categoryService; this.viewsService = viewsService; - this.userService = userService; + this.userService = userService; this.personFacade = personFacade; this.userId = this.userService.getLoggedInUser().getId(); addClassNames("expenses-view"); @@ -114,27 +117,38 @@ public class ExpensesView extends Div implements BeforeEnterObserver { grid.setPageSize(22); // setting page size grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); - // Filtering setup + // Filtering setup - Name nameFilter.setPlaceholder("Filter by Name..."); nameFilter.setClearButtonVisible(true); nameFilter.setValueChangeMode(ValueChangeMode.LAZY); - nameFilter.addValueChangeListener(e -> this.viewsService.applyFilter(nameFilter, userId, grid)); + nameFilter.addValueChangeListener(e -> this.viewsService.applyNameFilter(nameFilter, userId, grid)); - // Add nameFilter field to layout (above the grid) + // Filtering setup - Category + categoryFilter.setPlaceholder("Filter by Category..."); + categoryFilter.setClearButtonVisible(true); + categoryFilter.setItems(categoryService.findAllByUserId(userId)); + categoryFilter.setItemLabelGenerator(Category::getName); + categoryFilter.addValueChangeListener(e -> this.viewsService.applyCategoryFilter(categoryFilter, userId, grid)); + + // Add filter fields to layout (above the grid) VerticalLayout layout = new VerticalLayout(); - layout.add(nameFilter, grid); + HorizontalLayout filterLayout = new HorizontalLayout(nameFilter, categoryFilter); + filterLayout.setSpacing(true); + filterLayout.setAlignItems(FlexComponent.Alignment.BASELINE); + layout.add(filterLayout, grid); splitLayout.addToPrimary(layout); // when a row is selected or deselected, populate form grid.asSingleSelect().addValueChangeListener(event -> { - if (event.getValue() != null) UI.getCurrent().navigate(String.format(EXPENSE_EDIT_ROUTE_TEMPLATE, event.getValue().getId())); - else { + if (event.getValue() != null) { + UI.getCurrent().navigate(String.format(EXPENSE_EDIT_ROUTE_TEMPLATE, event.getValue().getId())); + } else { clearForm(); UI.getCurrent().navigate(ExpensesView.class); } }); - // Bind fields. This is where you'd define e.g. validation rules + // Configure Form binder = new BeanValidationBinder<>(Expense.class); binder.bindInstanceFields(this); binder.forField(name) @@ -171,22 +185,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver { } }); -// TODO:// Event listeners that will remove the selected creditors from the debtors list and vice versa -// // Done so that the user cant create an expense with the same person as creditor and debtor -// payer.addValueChangeListener(event -> { -// Person selectedDebtors = event.getValue(); -// final var creditorsSet = new HashSet<>(personService.findAllWithoutUser()); -// creditorsSet.removeIf(creditorsSet::contains); -// payer.setItems(creditorsSet); -// }); -// -// beneficiary.addValueChangeListener(event -> { -// Person selectedCreditors = event.getValue(); -// final var debtorsSet = new HashSet<>(personService.findAllWithoutUser()); -// debtorsSet.removeIf(debtorsSet::contains); -// beneficiary.setItems(debtorsSet); -// }); - cancel.addClickListener(e -> { clearForm(); refreshGrid(); @@ -227,7 +225,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver { } }); - // Initialize ComboBox with the logged-in user's Person entity as default initializeComboBoxes(); } @@ -242,8 +239,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver { Notification.show( String.format("The requested expense was not found, ID = %s", expenseId.get()), 3000, Notification.Position.BOTTOM_START); - // when a row is selected but the data is no longer available, - // refresh grid refreshGrid(); event.forwardTo(ExpensesView.class); }