Compare commits

..

No commits in common. "0fc5b07f5edffef49426d3f8b7d1792a0e49cfbc" and "751aa257b14332c3715f673e93734ad67201cc3b" have entirely different histories.

7 changed files with 35 additions and 82 deletions

View file

@ -61,16 +61,15 @@ Munera is a companion for managing expenses efficiently and effortlessly, whethe
6. **Misc** 6. **Misc**
- PeriodUnit and Interval need to be implemented with a scheduler - PeriodUnit and Interval need to be implemented with a scheduler
- Graphs could use more work, maybe some filtering? - Graphs could use more work
- More validation in form - More validation in form
- ~~Login page~~ - Login page
- email setup
- Migration tool for DB changes in prod - Migration tool for DB changes in prod
## Known Issues ## 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. - 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 - Errors need to be caught and handled
- Graphs still need ~~a lot of~~ **some** improvements - 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~~ - ~~ExpenseView dosent refresh after an edit operation anymore~~

View file

@ -1,10 +0,0 @@
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 @Getter
@Setter @Setter
@Table(name = "expenses") @Table(name = "expenses")
public class Expense extends AbstractEntity { public class Expense {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

View file

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

View file

@ -11,7 +11,6 @@ import org.springframework.stereotype.Service;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
@Service @Service
public class ExpenseService { public class ExpenseService {
@ -41,13 +40,6 @@ public class ExpenseService {
public Collection<Expense> findUnpaidCreditByUser(final Person person) { public Collection<Expense> findUnpaidCreditByUser(final Person person) {
return repository.findUnpaidCreditorsExpensesByPersonId(person.getId()); 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 List<Expense> findAll() {return repository.findAll();}
public void update(Expense entity) { public void update(Expense entity) {

View file

@ -1,8 +1,6 @@
package com.application.munera.views.people; package com.application.munera.views.people;
import com.application.munera.data.Expense;
import com.application.munera.data.Person; import com.application.munera.data.Person;
import com.application.munera.services.ExpenseService;
import com.application.munera.services.PersonService; import com.application.munera.services.PersonService;
import com.application.munera.views.MainLayout; import com.application.munera.views.MainLayout;
import com.vaadin.flow.component.UI; import com.vaadin.flow.component.UI;
@ -10,6 +8,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.dependency.Uses; import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout; 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.grid.GridVariant;
import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.html.Span;
@ -21,7 +20,6 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.splitlayout.SplitLayout; import com.vaadin.flow.component.splitlayout.SplitLayout;
import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.EmailField;
import com.vaadin.flow.component.textfield.TextField; 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.BeanValidationBinder;
import com.vaadin.flow.data.binder.ValidationException; import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.data.renderer.ComponentRenderer;
@ -29,11 +27,12 @@ import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import java.util.Optional; import java.util.Optional;
@PageTitle("People") @PageTitle("People")
@ -45,7 +44,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
private static final String PERSON_ID = "personID"; private static final String PERSON_ID = "personID";
private static final String PERSON_EDIT_ROUTE_TEMPLATE = "people/%s/edit"; private static final String PERSON_EDIT_ROUTE_TEMPLATE = "people/%s/edit";
private final TreeGrid<Object> grid = new TreeGrid<>(); private final Grid<Person> grid = new Grid<>(Person.class, false);
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");
@ -55,14 +54,12 @@ public class PeopleView extends Div implements BeforeEnterObserver {
private Person person; private Person person;
private final PersonService personService; private final PersonService personService;
private final ExpenseService expenseService;
private TextField firstName; private TextField firstName;
private TextField lastName; private TextField lastName;
private EmailField email; private EmailField email;
public PeopleView(PersonService personService, ExpenseService expenseService) { public PeopleView(PersonService personService) {
this.personService = personService; this.personService = personService;
this.expenseService = expenseService;
addClassNames("expenses-view"); addClassNames("expenses-view");
// Create UI // Create UI
@ -73,22 +70,33 @@ public class PeopleView extends Div implements BeforeEnterObserver {
add(splitLayout); add(splitLayout);
grid.addThemeVariants(GridVariant.LUMO_NO_BORDER); // Configure Grid
grid.addHierarchyColumn(this::getNodeName).setHeader("Name"); grid.addColumn(Person::getFirstName).setHeader("First Name").setSortable(true);
grid.addColumn(this::getNodeCost).setHeader("Total Expenses Value").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 -> { grid.addColumn(new ComponentRenderer<>(persona -> {
if (persona instanceof Person) return createPersonBadge(personService.calculateNetBalance((Person) persona)); final var netBalance = personService.calculateNetBalance(persona);
else return createExpenseBadge(((Expense) persona).getIsResolved()); return createBadge(netBalance);
})).setHeader("Balance Status"); })).setHeader("Balance Status").setSortable(true);
List<Person> people = (List<Person>) personService.findAll();
this.setGridData(people); 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);
// 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 -> {
Object selectedItem = event.getValue(); if (event.getValue() != null) {
if (selectedItem instanceof Person selectedPerson) UI.getCurrent().navigate(String.format(PERSON_EDIT_ROUTE_TEMPLATE, selectedPerson.getId())); UI.getCurrent().navigate(String.format(PERSON_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
} else {
clearForm();
UI.getCurrent().navigate(PeopleView.class);
}
}); });
// Configure Form // Configure Form
@ -141,25 +149,14 @@ 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 @Override
public void beforeEnter(BeforeEnterEvent event) { public void beforeEnter(BeforeEnterEvent event) {
Optional<Long> personId = event.getRouteParameters().get(PERSON_ID).map(Long::parseLong); Optional<Long> personId = event.getRouteParameters().get(PERSON_ID).map(Long::parseLong);
if (personId.isPresent()) { if (personId.isPresent()) {
Optional<Person> personFromBackend = personService.get(personId.get()); Optional<Person> personFromBackend = personService.get(personId.get());
if (personFromBackend.isPresent()) populateForm(personFromBackend.get()); if (personFromBackend.isPresent()) {
else { populateForm(personFromBackend.get());
} else {
Notification.show( Notification.show(
String.format("The requested person was not found, ID = %s", personId.get()), 3000, String.format("The requested person was not found, ID = %s", personId.get()), 3000,
Position.BOTTOM_START); Position.BOTTOM_START);
@ -226,7 +223,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
} }
private Span createPersonBadge(BigDecimal netBalance) { private Span createBadge(BigDecimal netBalance) {
Span badge = new Span(); Span badge = new Span();
if (netBalance.compareTo(BigDecimal.ZERO) < 0) { if (netBalance.compareTo(BigDecimal.ZERO) < 0) {
badge.setText("Credit"); badge.setText("Credit");
@ -240,29 +237,4 @@ public class PeopleView extends Div implements BeforeEnterObserver {
} }
return badge; 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: 116 KiB

After

Width:  |  Height:  |  Size: 77 KiB