feat: expense category filtering

This commit is contained in:
effe 2024-09-15 16:55:28 -04:00
parent c70ea1c76c
commit 56e5928e4b
3 changed files with 55 additions and 29 deletions

View file

@ -5,6 +5,8 @@ import jakarta.validation.constraints.Size;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.Objects;
@Entity @Entity
@Getter @Getter
@Setter @Setter
@ -24,4 +26,17 @@ public class Category {
@Column(name = "userId", nullable = false) @Column(name = "userId", nullable = false)
private Long userId; 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);
}
} }

View file

@ -1,8 +1,10 @@
package com.application.munera.services; package com.application.munera.services;
import com.application.munera.data.BadgeMessage; import com.application.munera.data.BadgeMessage;
import com.application.munera.data.Category;
import com.application.munera.data.Expense; import com.application.munera.data.Expense;
import com.application.munera.data.ExpenseType; 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.html.Span;
import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.textfield.TextField;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -47,8 +49,8 @@ public class ViewsService {
return badge; return badge;
} }
public void applyFilter(TextField nameFilter, Long userId, PaginatedGrid<Expense, Objects> grid) { public void applyNameFilter(TextField nameFilter, Long userId, PaginatedGrid<Expense, Objects> grid) {
String filterValue = nameFilter.getValue().trim(); final var filterValue = nameFilter.getValue().trim();
List<Expense> filteredExpenses; List<Expense> filteredExpenses;
if (filterValue.isEmpty()) filteredExpenses = expenseService.findAllOrderByDateDescending(userId); // If the filter is empty, return all expenses if (filterValue.isEmpty()) filteredExpenses = expenseService.findAllOrderByDateDescending(userId); // If the filter is empty, return all expenses
else { else {
@ -60,6 +62,20 @@ public class ViewsService {
grid.setItems(filteredExpenses); grid.setItems(filteredExpenses);
} }
public void applyCategoryFilter(MultiSelectComboBox<Category> categoryFilter, Long userId, PaginatedGrid<Expense, Objects> grid) {
final var selectedCategories = categoryFilter.getValue();
List<Expense> 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) { private BadgeMessage determineBadgeMessage(ExpenseType type, boolean isPaid) {
return switch (type) { return switch (type) {
case CREDIT -> isPaid ? BadgeMessage.PAID_TO_ME : BadgeMessage.OWED_TO_ME; case CREDIT -> isPaid ? BadgeMessage.PAID_TO_ME : BadgeMessage.OWED_TO_ME;

View file

@ -15,6 +15,7 @@ import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.checkbox.Checkbox; import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.combobox.ComboBox; 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.datepicker.DatePicker;
import com.vaadin.flow.component.dependency.Uses; import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout; 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;
import com.vaadin.flow.component.notification.Notification.Position; import com.vaadin.flow.component.notification.Notification.Position;
import com.vaadin.flow.component.notification.NotificationVariant; 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.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.splitlayout.SplitLayout; import com.vaadin.flow.component.splitlayout.SplitLayout;
@ -56,6 +58,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
private final PaginatedGrid<Expense, Objects> grid = new PaginatedGrid<>(); private final PaginatedGrid<Expense, Objects> grid = new PaginatedGrid<>();
private final TextField nameFilter = new TextField(); private final TextField nameFilter = new TextField();
private final MultiSelectComboBox<Category> categoryFilter = new MultiSelectComboBox<>();
private final Button cancel = new Button("Cancel"); private final Button cancel = new Button("Cancel");
private final Button save = new Button("Save"); private final Button save = new Button("Save");
private final Button delete = new Button("Delete"); private final Button delete = new Button("Delete");
@ -86,7 +89,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
this.expenseService = expenseService; this.expenseService = expenseService;
this.categoryService = categoryService; this.categoryService = categoryService;
this.viewsService = viewsService; this.viewsService = viewsService;
this.userService = userService; this.userService = userService;
this.personFacade = personFacade; this.personFacade = personFacade;
this.userId = this.userService.getLoggedInUser().getId(); this.userId = this.userService.getLoggedInUser().getId();
addClassNames("expenses-view"); addClassNames("expenses-view");
@ -114,27 +117,38 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
grid.setPageSize(22); // setting page size grid.setPageSize(22); // setting page size
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
// Filtering setup // Filtering setup - Name
nameFilter.setPlaceholder("Filter by Name..."); nameFilter.setPlaceholder("Filter by Name...");
nameFilter.setClearButtonVisible(true); nameFilter.setClearButtonVisible(true);
nameFilter.setValueChangeMode(ValueChangeMode.LAZY); 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(); 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); splitLayout.addToPrimary(layout);
// when a row is selected or deselected, populate form // when a row is selected or deselected, populate form
grid.asSingleSelect().addValueChangeListener(event -> { grid.asSingleSelect().addValueChangeListener(event -> {
if (event.getValue() != null) UI.getCurrent().navigate(String.format(EXPENSE_EDIT_ROUTE_TEMPLATE, event.getValue().getId())); if (event.getValue() != null) {
else { UI.getCurrent().navigate(String.format(EXPENSE_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
} else {
clearForm(); clearForm();
UI.getCurrent().navigate(ExpensesView.class); 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 = new BeanValidationBinder<>(Expense.class);
binder.bindInstanceFields(this); binder.bindInstanceFields(this);
binder.forField(name) 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 -> { cancel.addClickListener(e -> {
clearForm(); clearForm();
refreshGrid(); 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(); initializeComboBoxes();
} }
@ -242,8 +239,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
Notification.show( Notification.show(
String.format("The requested expense was not found, ID = %s", expenseId.get()), 3000, String.format("The requested expense was not found, ID = %s", expenseId.get()), 3000,
Notification.Position.BOTTOM_START); Notification.Position.BOTTOM_START);
// when a row is selected but the data is no longer available,
// refresh grid
refreshGrid(); refreshGrid();
event.forwardTo(ExpensesView.class); event.forwardTo(ExpensesView.class);
} }