Compare commits

...

6 commits

Author SHA1 Message Date
filippo-ferrari
0fc5b07f5e refactor 2024-07-18 18:44:29 +02:00
filippo-ferrari
7450fe3857 docs: README.md 2024-07-18 18:37:47 +02:00
filippo-ferrari
d1c0b8a1d3 refactor 2024-07-18 18:33:59 +02:00
filippo-ferrari
eab92d3640 feat: TreeGrid second iteration
still needs sorting
2024-07-18 18:27:55 +02:00
filippo-ferrari
b7a6a2fdb3 feat: TreeGrid first iteration
work in progress
2024-07-18 17:59:08 +02:00
filippo-ferrari
2415858d52 docs: README.md 2024-07-15 23:25:38 +02:00
7 changed files with 84 additions and 37 deletions

View file

@ -61,15 +61,16 @@ Munera is a companion for managing expenses efficiently and effortlessly, whethe
6. **Misc**
- PeriodUnit and Interval need to be implemented with a scheduler
- Graphs could use more work
- Graphs could use more work, maybe some filtering?
- More validation in form
- Login page
- ~~Login page~~
- email setup
- Migration tool for DB changes in prod
## Known Issues
- PeopleVIew dosent refresh after an edit operation anymore
- Form still needs more validation when empty, some entities can be created with all null values, even the ones that have constraints throw SQL errors, they need to be gracefully handled.
- Errors need to be caught and handled
- Graphs still need ~~a lot of~~ **some** improvements
- PeriodUnit and Interval need to be implemented with a scheduler
- ~~ExpenseView dosent refresh after an edit operation anymore~~

View file

@ -0,0 +1,10 @@
package com.application.munera.data;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public abstract class AbstractEntity {
private Long id;
}

View file

@ -13,7 +13,7 @@ import java.util.Set;
@Getter
@Setter
@Table(name = "expenses")
public class Expense {
public class Expense extends AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View file

@ -14,7 +14,7 @@ import java.util.Set;
@Getter
@Setter
@Table(name = "people")
public class Person {
public class Person extends AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View file

@ -11,6 +11,7 @@ import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@Service
public class ExpenseService {
@ -40,6 +41,13 @@ public class ExpenseService {
public Collection<Expense> findUnpaidCreditByUser(final Person person) {
return repository.findUnpaidCreditorsExpensesByPersonId(person.getId());
}
public List<Expense> findExpenseByUser(final Person person) {
final var credits = this.findCreditByUser(person);
final var debits = this.findDebtByUser(person);
return Stream.concat(credits.stream(), debits.stream()).toList();
}
public List<Expense> findAll() {return repository.findAll();}
public void update(Expense entity) {

View file

@ -1,6 +1,8 @@
package com.application.munera.views.people;
import com.application.munera.data.Expense;
import com.application.munera.data.Person;
import com.application.munera.services.ExpenseService;
import com.application.munera.services.PersonService;
import com.application.munera.views.MainLayout;
import com.vaadin.flow.component.UI;
@ -8,7 +10,6 @@ import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
@ -20,6 +21,7 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.splitlayout.SplitLayout;
import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.data.renderer.ComponentRenderer;
@ -27,12 +29,11 @@ import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import jakarta.annotation.security.PermitAll;
import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
@PageTitle("People")
@ -44,7 +45,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
private static final String PERSON_ID = "personID";
private static final String PERSON_EDIT_ROUTE_TEMPLATE = "people/%s/edit";
private final Grid<Person> grid = new Grid<>(Person.class, false);
private final TreeGrid<Object> grid = new TreeGrid<>();
private final Button cancel = new Button("Cancel");
private final Button save = new Button("Save");
@ -54,12 +55,14 @@ public class PeopleView extends Div implements BeforeEnterObserver {
private Person person;
private final PersonService personService;
private final ExpenseService expenseService;
private TextField firstName;
private TextField lastName;
private EmailField email;
public PeopleView(PersonService personService) {
public PeopleView(PersonService personService, ExpenseService expenseService) {
this.personService = personService;
this.expenseService = expenseService;
addClassNames("expenses-view");
// Create UI
@ -70,33 +73,22 @@ public class PeopleView extends Div implements BeforeEnterObserver {
add(splitLayout);
// Configure Grid
grid.addColumn(Person::getFirstName).setHeader("First Name").setSortable(true);
grid.addColumn(Person::getLastName).setHeader("Last Name").setSortable(true);
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
grid.addColumn(personService::calculateDebt).setHeader("Debt").setSortable(true);
grid.addColumn(personService::calculateCredit).setHeader("Credit").setSortable(true);
grid.addColumn(personService::calculateNetBalance).setHeader("Total Expenses value").setSortable(true);
grid.addColumn(new ComponentRenderer<>(persona -> {
final var netBalance = personService.calculateNetBalance(persona);
return createBadge(netBalance);
})).setHeader("Balance Status").setSortable(true);
grid.getColumns().forEach(col -> col.setAutoWidth(true));
grid.setItems(query -> personService.list(
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
.stream());
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
grid.addHierarchyColumn(this::getNodeName).setHeader("Name");
grid.addColumn(this::getNodeCost).setHeader("Total Expenses Value").setSortable(true);
grid.addColumn(new ComponentRenderer<>(persona -> {
if (persona instanceof Person) return createPersonBadge(personService.calculateNetBalance((Person) persona));
else return createExpenseBadge(((Expense) persona).getIsResolved());
})).setHeader("Balance Status");
List<Person> people = (List<Person>) personService.findAll();
this.setGridData(people);
// when a row is selected or deselected, populate form
grid.asSingleSelect().addValueChangeListener(event -> {
if (event.getValue() != null) {
UI.getCurrent().navigate(String.format(PERSON_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
} else {
clearForm();
UI.getCurrent().navigate(PeopleView.class);
}
Object selectedItem = event.getValue();
if (selectedItem instanceof Person selectedPerson) UI.getCurrent().navigate(String.format(PERSON_EDIT_ROUTE_TEMPLATE, selectedPerson.getId()));
});
// Configure Form
@ -149,14 +141,25 @@ public class PeopleView extends Div implements BeforeEnterObserver {
});
}
private String getNodeName(Object node) {
if (node instanceof Person) return ((Person) node).getFirstName() + " " + ((Person) node).getLastName();
else if (node instanceof Expense) return ((Expense) node).getName();
return "";
}
private String getNodeCost(Object node) {
if (node instanceof Person) return this.personService.calculateNetBalance((Person) node).toString() + "";
else if (node instanceof Expense) return ((Expense) node).getCost().toString() + "";
return "";
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
Optional<Long> personId = event.getRouteParameters().get(PERSON_ID).map(Long::parseLong);
if (personId.isPresent()) {
Optional<Person> personFromBackend = personService.get(personId.get());
if (personFromBackend.isPresent()) {
populateForm(personFromBackend.get());
} else {
if (personFromBackend.isPresent()) populateForm(personFromBackend.get());
else {
Notification.show(
String.format("The requested person was not found, ID = %s", personId.get()), 3000,
Position.BOTTOM_START);
@ -223,7 +226,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
}
private Span createBadge(BigDecimal netBalance) {
private Span createPersonBadge(BigDecimal netBalance) {
Span badge = new Span();
if (netBalance.compareTo(BigDecimal.ZERO) < 0) {
badge.setText("Credit");
@ -237,4 +240,29 @@ public class PeopleView extends Div implements BeforeEnterObserver {
}
return badge;
}
private Span createExpenseBadge(Boolean isExpenseResolved) {
Span badge = new Span();
if (Boolean.TRUE.equals(isExpenseResolved)) {
badge.setText("Resolved");
badge.getElement().getThemeList().add("badge success");
} else {
badge.setText("To be Resolved");
badge.getElement().getThemeList().add("badge error");
}
return badge;
}
public void setGridData(List<Person> people) {
for (Person person : people) {
// Add the person as a root item
grid.getTreeData().addItem(null, person);
// Fetch expenses for the current person
List<Expense> expenses = expenseService.findExpenseByUser(person);
// Add each expense as a child item under the person
for (Expense expense : expenses) grid.getTreeData().addItem(person, expense);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 116 KiB