Compare commits
3 commits
f6ff57cc02
...
913ee13d33
Author | SHA1 | Date | |
---|---|---|---|
|
913ee13d33 | ||
|
69986a9027 | ||
|
0b31beee7b |
5 changed files with 206 additions and 33 deletions
|
@ -6,6 +6,7 @@ import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -37,4 +38,8 @@ public class CategoryService {
|
||||||
public Page<Category> list(Pageable pageable){
|
public Page<Category> list(Pageable pageable){
|
||||||
return categoryRepository.findAll(pageable);
|
return categoryRepository.findAll(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<String> findNames() {
|
||||||
|
return categoryRepository.findAll().stream().map(Category::getName).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -31,6 +32,10 @@ public class PersonService {
|
||||||
return this.personRepository.findAll();
|
return this.personRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Person> findAllAsList() {
|
||||||
|
return this.personRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
public void update(Person person) {
|
public void update(Person person) {
|
||||||
this.personRepository.save(person);
|
this.personRepository.save(person);
|
||||||
}
|
}
|
||||||
|
@ -64,4 +69,8 @@ public class PersonService {
|
||||||
final var debit = this.expenseService.findUnpaidDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
final var debit = this.expenseService.findUnpaidDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
return credit.subtract(debit);
|
return credit.subtract(debit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> findNames() {
|
||||||
|
return personRepository.findAll().stream().map(Person::getFirstName).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
private static final String EXPENSE_ID = "expenseID";
|
private static final String EXPENSE_ID = "expenseID";
|
||||||
private static final String EXPENSE_EDIT_ROUTE_TEMPLATE = "/%s/edit";
|
private static final String EXPENSE_EDIT_ROUTE_TEMPLATE = "/%s/edit";
|
||||||
|
|
||||||
private final Grid<Expense> grid = new Grid<>(Expense.class, false);
|
private Grid<Expense> grid;
|
||||||
|
|
||||||
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");
|
||||||
|
@ -74,6 +74,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
private MultiSelectComboBox<Person> creditors;
|
private MultiSelectComboBox<Person> creditors;
|
||||||
private MultiSelectComboBox<Person> debtors;
|
private MultiSelectComboBox<Person> debtors;
|
||||||
private ComboBox<Event> event;
|
private ComboBox<Event> event;
|
||||||
|
|
||||||
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService, EventService eventService) {
|
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService, EventService eventService) {
|
||||||
this.expenseService = expenseService;
|
this.expenseService = expenseService;
|
||||||
this.categoryService = categoryService;
|
this.categoryService = categoryService;
|
||||||
|
@ -81,41 +82,18 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
addClassNames("expenses-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
|
final var categoriesNames = this.categoryService.findAll();
|
||||||
|
final var creditorsNames = this.personService.findAllAsList();
|
||||||
|
Filters filters = new Filters(this::refreshGrid, categoriesNames, creditorsNames);
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
SplitLayout splitLayout = new SplitLayout();
|
SplitLayout splitLayout = new SplitLayout();
|
||||||
|
|
||||||
createGridLayout(splitLayout);
|
createGridLayout(splitLayout, filters);
|
||||||
createEditorLayout(splitLayout);
|
createEditorLayout(splitLayout);
|
||||||
|
|
||||||
add(splitLayout);
|
add(splitLayout);
|
||||||
|
|
||||||
// Configure Grid
|
|
||||||
grid.addColumn(Expense::getName).setHeader("Name").setSortable(true).setSortProperty("name");
|
|
||||||
grid.addColumn(Expense::getCost).setHeader("Amount").setSortable(true).setSortProperty("cost");
|
|
||||||
grid.addColumn(expenseCategory -> expenseCategory.getCategory().getName()).setHeader("Category").setSortable(true).setSortProperty("category");
|
|
||||||
grid.addColumn(Expense::getPeriodInterval).setHeader("Period Interval").setSortable(true);
|
|
||||||
grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true);
|
|
||||||
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
|
|
||||||
// grid.addColumn(expenseEvent -> expenseEvent.getEvent().getName()).setHeader("Event").setSortable(true);
|
|
||||||
|
|
||||||
grid.addColumn(new ComponentRenderer<>(expense1 -> createBadge(expenseService.isExpenseResolved(expense1)))).setHeader("Status").setSortable(true);
|
|
||||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
|
||||||
|
|
||||||
grid.setItems(query -> expenseService.list(
|
|
||||||
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
|
|
||||||
.stream());
|
|
||||||
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
clearForm();
|
|
||||||
UI.getCurrent().navigate(ExpensesView.class);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure Form
|
// Configure Form
|
||||||
binder = new BeanValidationBinder<>(Expense.class);
|
binder = new BeanValidationBinder<>(Expense.class);
|
||||||
|
|
||||||
|
@ -270,11 +248,38 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
editorLayoutDiv.add(buttonLayout);
|
editorLayoutDiv.add(buttonLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createGridLayout(SplitLayout splitLayout) {
|
private void createGridLayout(SplitLayout splitLayout, Filters filters) {
|
||||||
|
grid = new Grid<>(Expense.class, false);
|
||||||
Div wrapper = new Div();
|
Div wrapper = new Div();
|
||||||
wrapper.setClassName("grid-wrapper");
|
wrapper.setClassName("grid-wrapper");
|
||||||
splitLayout.addToPrimary(wrapper);
|
splitLayout.addToPrimary(wrapper);
|
||||||
wrapper.add(grid);
|
wrapper.add(filters, grid);
|
||||||
|
|
||||||
|
// Configure Grid
|
||||||
|
grid.addColumn(Expense::getName).setHeader("Name").setSortable(true).setSortProperty("name");
|
||||||
|
grid.addColumn(Expense::getCost).setHeader("Amount").setSortable(true).setSortProperty("cost");
|
||||||
|
grid.addColumn(expenseCategory -> expenseCategory.getCategory().getName()).setHeader("Category").setSortable(true).setSortProperty("category");
|
||||||
|
grid.addColumn(Expense::getPeriodInterval).setHeader("Period Interval").setSortable(true);
|
||||||
|
grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true);
|
||||||
|
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
|
||||||
|
|
||||||
|
grid.addColumn(new ComponentRenderer<>(expense1 -> createBadge(expenseService.isExpenseResolved(expense1)))).setHeader("Status").setSortable(true);
|
||||||
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
|
grid.setItems(query -> expenseService.list(
|
||||||
|
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
|
||||||
|
.stream());
|
||||||
|
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
clearForm();
|
||||||
|
UI.getCurrent().navigate(ExpensesView.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshGrid() {
|
private void refreshGrid() {
|
||||||
|
|
154
src/main/java/com/application/munera/views/expenses/Filters.java
Normal file
154
src/main/java/com/application/munera/views/expenses/Filters.java
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
package com.application.munera.views.expenses;
|
||||||
|
|
||||||
|
import com.application.munera.data.Category;
|
||||||
|
import com.application.munera.data.Expense;
|
||||||
|
import com.application.munera.data.Person;
|
||||||
|
import com.vaadin.flow.component.Component;
|
||||||
|
import com.vaadin.flow.component.Text;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.checkbox.CheckboxGroup;
|
||||||
|
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.html.Div;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.FlexLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
|
import com.vaadin.flow.theme.lumo.LumoUtility;
|
||||||
|
import jakarta.persistence.criteria.*;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Filters extends Div implements Specification<Expense> {
|
||||||
|
|
||||||
|
private final TextField name = new TextField("Expense's name");
|
||||||
|
private final ComboBox<Category> category = new ComboBox<>("Category");
|
||||||
|
private final DatePicker startDate = new DatePicker("Date of Birth");
|
||||||
|
private final DatePicker endDate = new DatePicker();
|
||||||
|
private final MultiSelectComboBox<Person> creditors = new MultiSelectComboBox<>("Creditors");
|
||||||
|
private final CheckboxGroup<String> isResolved = new CheckboxGroup<>("Role");
|
||||||
|
|
||||||
|
public Filters(Runnable onSearch, List<Category> categoriesNames, List<Person> creditorsNames) {
|
||||||
|
setWidthFull();
|
||||||
|
addClassName("filter-layout");
|
||||||
|
addClassNames(LumoUtility.Padding.Horizontal.LARGE, LumoUtility.Padding.Vertical.MEDIUM,
|
||||||
|
LumoUtility.BoxSizing.BORDER);
|
||||||
|
name.setPlaceholder("Expense's name");
|
||||||
|
final var names = creditorsNames.stream().map(Person::getFirstName).toArray();
|
||||||
|
creditors.setItems((creditorsNames));
|
||||||
|
|
||||||
|
isResolved.setItems("Worker", "Supervisor", "Manager", "External");
|
||||||
|
isResolved.addClassName("double-width");
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Button resetBtn = new Button("Reset");
|
||||||
|
resetBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
resetBtn.addClickListener(e -> {
|
||||||
|
name.clear();
|
||||||
|
category.clear();
|
||||||
|
startDate.clear();
|
||||||
|
endDate.clear();
|
||||||
|
creditors.clear();
|
||||||
|
isResolved.clear();
|
||||||
|
onSearch.run();
|
||||||
|
});
|
||||||
|
Button searchBtn = new Button("Search");
|
||||||
|
searchBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
searchBtn.addClickListener(e -> onSearch.run());
|
||||||
|
|
||||||
|
Div actions = new Div(resetBtn, searchBtn);
|
||||||
|
actions.addClassName(LumoUtility.Gap.SMALL);
|
||||||
|
actions.addClassName("actions");
|
||||||
|
|
||||||
|
add(name, category, creditors, isResolved, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Component createDateRangeFilter() {
|
||||||
|
startDate.setPlaceholder("From");
|
||||||
|
|
||||||
|
endDate.setPlaceholder("To");
|
||||||
|
|
||||||
|
// For screen readers
|
||||||
|
startDate.setAriaLabel("From date");
|
||||||
|
endDate.setAriaLabel("To date");
|
||||||
|
|
||||||
|
FlexLayout dateRangeComponent = new FlexLayout(startDate, new Text(" – "), endDate);
|
||||||
|
dateRangeComponent.setAlignItems(FlexComponent.Alignment.BASELINE);
|
||||||
|
dateRangeComponent.addClassName(LumoUtility.Gap.XSMALL);
|
||||||
|
|
||||||
|
return dateRangeComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Predicate toPredicate(Root<Expense> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
|
||||||
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
String lowerCaseFilter = name.getValue().toLowerCase();
|
||||||
|
Predicate firstNameMatch = criteriaBuilder.like(criteriaBuilder.lower(root.get("name")),
|
||||||
|
lowerCaseFilter + "%");
|
||||||
|
predicates.add(firstNameMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!category.isEmpty()) {
|
||||||
|
// String databaseColumn = "phone";
|
||||||
|
// String ignore = "- ()";
|
||||||
|
//
|
||||||
|
// String lowerCaseFilter = ignoreCharacters(ignore, category.getValue().toLowerCase());
|
||||||
|
// Predicate phoneMatch = criteriaBuilder.like(
|
||||||
|
// ignoreCharacters(ignore, criteriaBuilder, criteriaBuilder.lower(root.get(databaseColumn))),
|
||||||
|
// "%" + lowerCaseFilter + "%");
|
||||||
|
// predicates.add(phoneMatch);
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
if (startDate.getValue() != null) {
|
||||||
|
String databaseColumn = "dateOfBirth";
|
||||||
|
predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get(databaseColumn),
|
||||||
|
criteriaBuilder.literal(startDate.getValue())));
|
||||||
|
}
|
||||||
|
if (endDate.getValue() != null) {
|
||||||
|
String databaseColumn = "dateOfBirth";
|
||||||
|
predicates.add(criteriaBuilder.greaterThanOrEqualTo(criteriaBuilder.literal(endDate.getValue()),
|
||||||
|
root.get(databaseColumn)));
|
||||||
|
}
|
||||||
|
// if (!creditors.isEmpty()) {
|
||||||
|
// String databaseColumn = "occupation";
|
||||||
|
// List<Predicate> occupationPredicates = new ArrayList<>();
|
||||||
|
// for (String occupation : creditors.getValue()) {
|
||||||
|
// occupationPredicates
|
||||||
|
// .add(criteriaBuilder.equal(criteriaBuilder.literal(occupation), root.get(databaseColumn)));
|
||||||
|
// }
|
||||||
|
// predicates.add(criteriaBuilder.or(occupationPredicates.toArray(Predicate[]::new)));
|
||||||
|
// }
|
||||||
|
if (!isResolved.isEmpty()) {
|
||||||
|
String databaseColumn = "role";
|
||||||
|
List<Predicate> rolePredicates = new ArrayList<>();
|
||||||
|
for (String role : isResolved.getValue()) {
|
||||||
|
rolePredicates.add(criteriaBuilder.equal(criteriaBuilder.literal(role), root.get(databaseColumn)));
|
||||||
|
}
|
||||||
|
predicates.add(criteriaBuilder.or(rolePredicates.toArray(Predicate[]::new)));
|
||||||
|
}
|
||||||
|
return criteriaBuilder.and(predicates.toArray(Predicate[]::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String ignoreCharacters(String characters, String in) {
|
||||||
|
String result = in;
|
||||||
|
for (int i = 0; i < characters.length(); i++) {
|
||||||
|
result = result.replace("" + characters.charAt(i), "");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression<String> ignoreCharacters(String characters, CriteriaBuilder criteriaBuilder,
|
||||||
|
Expression<String> inExpression) {
|
||||||
|
Expression<String> expression = inExpression;
|
||||||
|
for (int i = 0; i < characters.length(); i++) {
|
||||||
|
expression = criteriaBuilder.function("replace", String.class, expression,
|
||||||
|
criteriaBuilder.literal(characters.charAt(i)), criteriaBuilder.literal(""));
|
||||||
|
}
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,5 +12,5 @@ spring.jpa.hibernate.ddl-auto = update
|
||||||
# To improve the performance during development.
|
# To improve the performance during development.
|
||||||
# For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters
|
# For more information https://vaadin.com/docs/latest/integrations/spring/configuration#special-configuration-parameters
|
||||||
vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.application.munera
|
vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.application.munera
|
||||||
#spring.jpa.defer-datasource-initialization = true
|
spring.jpa.defer-datasource-initialization = true
|
||||||
spring.sql.init.mode = always
|
spring.sql.init.mode = always
|
||||||
|
|
Loading…
Reference in a new issue