Compare commits
10 commits
18680a0f90
...
3595da2004
Author | SHA1 | Date | |
---|---|---|---|
|
3595da2004 | ||
|
a656f87741 | ||
|
9058be6a35 | ||
|
f1650ba542 | ||
|
02e81b4b63 | ||
|
d54c1ce379 | ||
|
8369ed6439 | ||
|
5ec3fef8fd | ||
|
d6706d78fb | ||
|
fee0309c4a |
14 changed files with 380 additions and 27 deletions
23
frontend/index.html
Normal file
23
frontend/index.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
This file is auto-generated by Vaadin.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
body, #outlet {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- This outlet div is where the views are rendered -->
|
||||||
|
<div id="outlet"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -8,14 +8,13 @@ import lombok.Setter;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Setter
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
@Table(name = "events")
|
@Table(name = "events")
|
||||||
public class Event {
|
public class Event {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@Column(name = "Id", unique = true, nullable = false)
|
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Size(max = 100)
|
@Size(max = 100)
|
||||||
|
|
|
@ -57,6 +57,10 @@ public class Expense {
|
||||||
inverseJoinColumns = @JoinColumn(name = "people_id"))
|
inverseJoinColumns = @JoinColumn(name = "people_id"))
|
||||||
private Set<Person> debtors;
|
private Set<Person> debtors;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "EventId")
|
||||||
|
private Event event;
|
||||||
|
|
||||||
@Column(name = "Date", nullable = false, columnDefinition = "DATE DEFAULT CURRENT_DATE")
|
@Column(name = "Date", nullable = false, columnDefinition = "DATE DEFAULT CURRENT_DATE")
|
||||||
private LocalDate date;
|
private LocalDate date;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -46,5 +47,24 @@ public class Person {
|
||||||
private Set<Expense> debtorExpenses;
|
private Set<Expense> debtorExpenses;
|
||||||
|
|
||||||
@ManyToMany(mappedBy = "participants")
|
@ManyToMany(mappedBy = "participants")
|
||||||
private Set<Event> participants;
|
private Set<Event> events;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(obj instanceof Person)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Person other = (Person) obj;
|
||||||
|
return Objects.equals(firstName, other.firstName) &&
|
||||||
|
Objects.equals(lastName, other.lastName) &&
|
||||||
|
Objects.equals(email, other.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(firstName, lastName, email);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
package com.application.munera.repositories;
|
package com.application.munera.repositories;
|
||||||
|
|
||||||
import com.application.munera.data.Category;
|
import com.application.munera.data.Category;
|
||||||
import com.application.munera.data.Expense;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Expense> {
|
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Category> {
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.application.munera.repositories;
|
||||||
|
|
||||||
|
import com.application.munera.data.Event;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface EventRepository extends JpaRepository<Event, Long>, JpaSpecificationExecutor<Event> {
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.application.munera.services;
|
||||||
|
|
||||||
|
import com.application.munera.data.Event;
|
||||||
|
import com.application.munera.repositories.EventRepository;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EventService {
|
||||||
|
|
||||||
|
private final EventRepository eventRepository;
|
||||||
|
|
||||||
|
public EventService(final EventRepository eventRepository){
|
||||||
|
this.eventRepository = eventRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Event> findById(Long id) {
|
||||||
|
return eventRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Event> findAll() {
|
||||||
|
return eventRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(Event event) {
|
||||||
|
eventRepository.save(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Event event) {
|
||||||
|
eventRepository.delete(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<Event> list(Pageable pageable){
|
||||||
|
return eventRepository.findAll(pageable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.Collection;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -27,7 +27,7 @@ public class PersonService {
|
||||||
return personRepository.findById(id);
|
return personRepository.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Person> findAll() {
|
public Collection<Person> findAll() {
|
||||||
return this.personRepository.findAll();
|
return this.personRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,9 +59,9 @@ public class PersonService {
|
||||||
return this.expenseService.findCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
return this.expenseService.findCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal calculateTotalExpenses(final Person person) {
|
public BigDecimal calculateNetBalance(final Person person) {
|
||||||
final var credit = this.expenseService.findCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
final var credit = this.expenseService.findCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
final var debit = this.expenseService.findDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
final var debit = this.expenseService.findDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
return credit.add(debit);
|
return credit.subtract(debit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.application.munera.views;
|
package com.application.munera.views;
|
||||||
|
|
||||||
import com.application.munera.views.expenses.CategoriesView;
|
import com.application.munera.views.expenses.CategoriesView;
|
||||||
|
import com.application.munera.views.expenses.EventsView;
|
||||||
import com.application.munera.views.expenses.ExpensesView;
|
import com.application.munera.views.expenses.ExpensesView;
|
||||||
import com.application.munera.views.expenses.PeopleView;
|
import com.application.munera.views.expenses.PeopleView;
|
||||||
import com.vaadin.flow.component.applayout.AppLayout;
|
import com.vaadin.flow.component.applayout.AppLayout;
|
||||||
|
@ -55,6 +56,7 @@ public class MainLayout extends AppLayout {
|
||||||
nav.addItem(new SideNavItem("Expenses", ExpensesView.class, LineAwesomeIcon.MONEY_BILL_SOLID.create()));
|
nav.addItem(new SideNavItem("Expenses", ExpensesView.class, LineAwesomeIcon.MONEY_BILL_SOLID.create()));
|
||||||
nav.addItem(new SideNavItem("Categories", CategoriesView.class, LineAwesomeIcon.FOLDER.create()));
|
nav.addItem(new SideNavItem("Categories", CategoriesView.class, LineAwesomeIcon.FOLDER.create()));
|
||||||
nav.addItem(new SideNavItem("People", PeopleView.class, LineAwesomeIcon.USER.create()));
|
nav.addItem(new SideNavItem("People", PeopleView.class, LineAwesomeIcon.USER.create()));
|
||||||
|
nav.addItem(new SideNavItem("Events", EventsView.class, LineAwesomeIcon.BANDCAMP.create()));
|
||||||
|
|
||||||
return nav;
|
return nav;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
public CategoriesView(CategoryService categoryService) {
|
public CategoriesView(CategoryService categoryService) {
|
||||||
this.categoryService = categoryService;
|
this.categoryService = categoryService;
|
||||||
addClassNames("categories-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
SplitLayout splitLayout = new SplitLayout();
|
SplitLayout splitLayout = new SplitLayout();
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
package com.application.munera.views.expenses;
|
||||||
|
|
||||||
|
import com.application.munera.data.Event;
|
||||||
|
import com.application.munera.data.Person;
|
||||||
|
import com.application.munera.services.EventService;
|
||||||
|
import com.application.munera.services.PersonService;
|
||||||
|
import com.application.munera.views.MainLayout;
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
|
import com.vaadin.flow.component.button.Button;
|
||||||
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
|
import com.vaadin.flow.component.combobox.MultiSelectComboBox;
|
||||||
|
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.icon.Icon;
|
||||||
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
|
import com.vaadin.flow.component.notification.NotificationVariant;
|
||||||
|
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||||
|
import com.vaadin.flow.component.splitlayout.SplitLayout;
|
||||||
|
import com.vaadin.flow.component.textfield.TextArea;
|
||||||
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
|
import com.vaadin.flow.data.binder.BeanValidationBinder;
|
||||||
|
import com.vaadin.flow.data.binder.ValidationException;
|
||||||
|
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 org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@PageTitle("Events")
|
||||||
|
@Route(value = "events/:eventID?/:action?(edit)", layout = MainLayout.class)
|
||||||
|
@Uses(Icon.class)
|
||||||
|
public class EventsView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
|
private static final String EVENT_ID = "eventID";
|
||||||
|
private static final String EVENT_EDIT_ROUTE_TEMPLATE = "events/%s/edit";
|
||||||
|
|
||||||
|
private final Grid<Event> grid = new Grid<>(Event.class, false);
|
||||||
|
|
||||||
|
private final Button cancel = new Button("Cancel");
|
||||||
|
private final Button save = new Button("Save");
|
||||||
|
private final Button delete = new Button("Delete");
|
||||||
|
|
||||||
|
private final BeanValidationBinder<Event> binder;
|
||||||
|
|
||||||
|
private Event event;
|
||||||
|
private final EventService eventService;
|
||||||
|
private final PersonService personService;
|
||||||
|
private TextField name;
|
||||||
|
private TextArea description;
|
||||||
|
private MultiSelectComboBox<Person> participants;
|
||||||
|
|
||||||
|
public EventsView(EventService eventService, PersonService personService) {
|
||||||
|
this.eventService = eventService;
|
||||||
|
this.personService = personService;
|
||||||
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
|
// Create UI
|
||||||
|
SplitLayout splitLayout = new SplitLayout();
|
||||||
|
|
||||||
|
createGridLayout(splitLayout);
|
||||||
|
createEditorLayout(splitLayout);
|
||||||
|
|
||||||
|
add(splitLayout);
|
||||||
|
|
||||||
|
// Configure Grid
|
||||||
|
grid.addColumn(Event::getName).setHeader("Name").setSortable(true);
|
||||||
|
grid.addColumn(Event::getDescription).setHeader("Description").setSortable(true);
|
||||||
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
|
grid.setItems(query -> eventService.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(EVENT_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
|
||||||
|
else {
|
||||||
|
clearForm();
|
||||||
|
UI.getCurrent().navigate(EventsView.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure Form
|
||||||
|
binder = new BeanValidationBinder<>(Event.class);
|
||||||
|
|
||||||
|
// Bind fields. This is where you'd define e.g. validation rules
|
||||||
|
|
||||||
|
binder.bindInstanceFields(this);
|
||||||
|
|
||||||
|
cancel.addClickListener(e -> {
|
||||||
|
clearForm();
|
||||||
|
refreshGrid();
|
||||||
|
});
|
||||||
|
|
||||||
|
save.addClickListener(e -> {
|
||||||
|
try {
|
||||||
|
if (this.event == null) {
|
||||||
|
this.event = new Event();
|
||||||
|
}
|
||||||
|
binder.writeBean(this.event);
|
||||||
|
eventService.update(this.event);
|
||||||
|
clearForm();
|
||||||
|
refreshGrid();
|
||||||
|
Notification.show("Data updated");
|
||||||
|
UI.getCurrent().navigate(EventsView.class);
|
||||||
|
} catch (ObjectOptimisticLockingFailureException exception) {
|
||||||
|
Notification n = Notification.show(
|
||||||
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
|
n.setPosition(Notification.Position.MIDDLE);
|
||||||
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
} catch (ValidationException validationException) {
|
||||||
|
Notification.show("Failed to update the data. Check again that all values are valid");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delete.addClickListener(e -> {
|
||||||
|
try {
|
||||||
|
if (this.event == null) throw new RuntimeException("Event is null!"); //TODO: create proper exception
|
||||||
|
eventService.delete(this.event);
|
||||||
|
clearForm();
|
||||||
|
refreshGrid();
|
||||||
|
Notification.show("Data deleted");
|
||||||
|
UI.getCurrent().navigate(EventsView.class);
|
||||||
|
} catch (ObjectOptimisticLockingFailureException exception) {
|
||||||
|
Notification n = Notification.show(
|
||||||
|
"Error updating the data. Somebody else has updated the record while you were making changes.");
|
||||||
|
n.setPosition(Notification.Position.MIDDLE);
|
||||||
|
n.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeEnter(BeforeEnterEvent event) {
|
||||||
|
Optional<Long> eventId = event.getRouteParameters().get(EVENT_ID).map(Long::parseLong);
|
||||||
|
if (eventId.isPresent()) {
|
||||||
|
Optional<Event> eventFromBackend = eventService.findById(eventId.get());
|
||||||
|
if (eventFromBackend.isPresent()) {
|
||||||
|
populateForm(eventFromBackend.get());
|
||||||
|
} else {
|
||||||
|
Notification.show(
|
||||||
|
String.format("The requested event was not found, ID = %s", eventId.get()), 3000,
|
||||||
|
Notification.Position.BOTTOM_START);
|
||||||
|
// when a row is selected but the data is no longer available,
|
||||||
|
// refresh grid
|
||||||
|
refreshGrid();
|
||||||
|
event.forwardTo(EventsView.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createEditorLayout(SplitLayout splitLayout) {
|
||||||
|
Div editorLayoutDiv = new Div();
|
||||||
|
editorLayoutDiv.setClassName("editor-layout");
|
||||||
|
|
||||||
|
Div editorDiv = new Div();
|
||||||
|
editorDiv.setClassName("editor");
|
||||||
|
editorLayoutDiv.add(editorDiv);
|
||||||
|
|
||||||
|
FormLayout formLayout = new FormLayout();
|
||||||
|
name = new TextField("Name");
|
||||||
|
description = new TextArea("Description");
|
||||||
|
participants = new MultiSelectComboBox<>("Participants");
|
||||||
|
participants.setItems(personService.findAll());
|
||||||
|
participants.setItemLabelGenerator(Person::getFirstName);
|
||||||
|
formLayout.add(name, description, participants);
|
||||||
|
editorDiv.add(formLayout);
|
||||||
|
createButtonLayout(editorLayoutDiv);
|
||||||
|
|
||||||
|
splitLayout.addToSecondary(editorLayoutDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createButtonLayout(Div editorLayoutDiv) {
|
||||||
|
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||||
|
buttonLayout.setClassName("button-layout");
|
||||||
|
cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
|
||||||
|
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
delete.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
buttonLayout.add(save, delete, cancel);
|
||||||
|
editorLayoutDiv.add(buttonLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createGridLayout(SplitLayout splitLayout) {
|
||||||
|
Div wrapper = new Div();
|
||||||
|
wrapper.setClassName("grid-wrapper");
|
||||||
|
splitLayout.addToPrimary(wrapper);
|
||||||
|
wrapper.add(grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshGrid() {
|
||||||
|
grid.select(null);
|
||||||
|
grid.getDataProvider().refreshAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
populateForm(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateForm(Event value) {
|
||||||
|
this.event = value;
|
||||||
|
binder.readBean(this.event);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
package com.application.munera.views.expenses;
|
package com.application.munera.views.expenses;
|
||||||
import com.application.munera.data.Category;
|
|
||||||
import com.application.munera.data.Expense;
|
import com.application.munera.data.*;
|
||||||
import com.application.munera.data.PeriodUnit;
|
|
||||||
import com.application.munera.data.Person;
|
|
||||||
import com.application.munera.services.CategoryService;
|
import com.application.munera.services.CategoryService;
|
||||||
|
import com.application.munera.services.EventService;
|
||||||
import com.application.munera.services.ExpenseService;
|
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;
|
||||||
|
@ -35,8 +34,7 @@ import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@PageTitle("Expenses")
|
@PageTitle("Expenses")
|
||||||
@Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class)
|
@Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class)
|
||||||
|
@ -59,6 +57,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
private final ExpenseService expenseService;
|
private final ExpenseService expenseService;
|
||||||
private final CategoryService categoryService;
|
private final CategoryService categoryService;
|
||||||
private final PersonService personService;
|
private final PersonService personService;
|
||||||
|
private final EventService eventService;
|
||||||
private TextField name;
|
private TextField name;
|
||||||
private TextField cost;
|
private TextField cost;
|
||||||
private ComboBox<Category> category;
|
private ComboBox<Category> category;
|
||||||
|
@ -69,11 +68,12 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
private DatePicker date;
|
private DatePicker date;
|
||||||
private MultiSelectComboBox<Person> creditors;
|
private MultiSelectComboBox<Person> creditors;
|
||||||
private MultiSelectComboBox<Person> debtors;
|
private MultiSelectComboBox<Person> debtors;
|
||||||
|
private ComboBox<Event> event;
|
||||||
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService) {
|
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService, EventService eventService) {
|
||||||
this.expenseService = expenseService;
|
this.expenseService = expenseService;
|
||||||
this.categoryService = categoryService;
|
this.categoryService = categoryService;
|
||||||
this.personService = personService;
|
this.personService = personService;
|
||||||
|
this.eventService = eventService;
|
||||||
addClassNames("expenses-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
|
@ -91,6 +91,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
grid.addColumn(Expense::getPeriodInterval).setHeader("Period Interval").setSortable(true);
|
grid.addColumn(Expense::getPeriodInterval).setHeader("Period Interval").setSortable(true);
|
||||||
grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true);
|
grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true);
|
||||||
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
|
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
|
||||||
|
// grid.addColumn(expenseEvent -> expenseEvent.getEvent().getName()).setHeader("Event").setSortable(true);
|
||||||
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
grid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
grid.setItems(query -> expenseService.list(
|
grid.setItems(query -> expenseService.list(
|
||||||
|
@ -115,9 +116,9 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
binder.bindInstanceFields(this);
|
binder.bindInstanceFields(this);
|
||||||
|
|
||||||
// We set initial value of isPeriodic to true and show period fields
|
// We set initial value of isPeriodic to true and show period fields
|
||||||
isPeriodic.setValue(true);
|
isPeriodic.setValue(false);
|
||||||
periodUnit.setVisible(true);
|
periodUnit.setVisible(false);
|
||||||
periodInterval.setVisible(true);
|
periodInterval.setVisible(false);
|
||||||
|
|
||||||
// We show the periodic fields only when the isPeriodic boolean is true
|
// We show the periodic fields only when the isPeriodic boolean is true
|
||||||
isPeriodic.addValueChangeListener(event -> {
|
isPeriodic.addValueChangeListener(event -> {
|
||||||
|
@ -132,6 +133,22 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
debtors.addValueChangeListener(event -> {
|
||||||
|
Set<Person> selectedDebtors = event.getValue();
|
||||||
|
final var creditorsSet = new HashSet<>(personService.findAll());
|
||||||
|
creditorsSet.removeIf(selectedDebtors::contains);
|
||||||
|
creditors.setItems(creditorsSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
creditors.addValueChangeListener(event -> {
|
||||||
|
Set<Person> selectedCreditors = event.getValue();
|
||||||
|
final var debtorsSet = new HashSet<>(personService.findAll());
|
||||||
|
debtorsSet.removeIf(selectedCreditors::contains);
|
||||||
|
debtors.setItems(debtorsSet);
|
||||||
|
});
|
||||||
|
|
||||||
cancel.addClickListener(e -> {
|
cancel.addClickListener(e -> {
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
|
@ -215,6 +232,9 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
creditors = new MultiSelectComboBox<>("Creditors");
|
creditors = new MultiSelectComboBox<>("Creditors");
|
||||||
creditors.setItems(personService.findAll());
|
creditors.setItems(personService.findAll());
|
||||||
creditors.setItemLabelGenerator(Person::getFirstName);
|
creditors.setItemLabelGenerator(Person::getFirstName);
|
||||||
|
event = new ComboBox<>("Event");
|
||||||
|
event.setItems(eventService.findAll());
|
||||||
|
event.setItemLabelGenerator(Event::getName);
|
||||||
debtors = new MultiSelectComboBox<>("Debtors");
|
debtors = new MultiSelectComboBox<>("Debtors");
|
||||||
debtors.setItems(personService.findAll());
|
debtors.setItems(personService.findAll());
|
||||||
debtors.setItemLabelGenerator(Person::getFirstName);
|
debtors.setItemLabelGenerator(Person::getFirstName);
|
||||||
|
@ -226,7 +246,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
|
||||||
? "var(--lumo-primary-text-color)"
|
? "var(--lumo-primary-text-color)"
|
||||||
: "var(--lumo-disabled-text-color)");
|
: "var(--lumo-disabled-text-color)");
|
||||||
|
|
||||||
formLayout.add(name, cost, category, description, isPeriodic, periodUnit, periodInterval, date, creditors, debtors);
|
formLayout.add(name, cost, category, description, isPeriodic, periodUnit, periodInterval, date, creditors, debtors, event);
|
||||||
grid.addColumn(isPeriodicRenderer).setHeader("Periodic").setAutoWidth(true);
|
grid.addColumn(isPeriodicRenderer).setHeader("Periodic").setAutoWidth(true);
|
||||||
editorDiv.add(formLayout);
|
editorDiv.add(formLayout);
|
||||||
createButtonLayout(editorLayoutDiv);
|
createButtonLayout(editorLayoutDiv);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.vaadin.flow.component.formlayout.FormLayout;
|
||||||
import com.vaadin.flow.component.grid.Grid;
|
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.icon.Icon;
|
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;
|
||||||
|
@ -21,6 +22,7 @@ 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.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.router.BeforeEnterEvent;
|
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;
|
||||||
|
@ -29,6 +31,7 @@ import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@PageTitle("People")
|
@PageTitle("People")
|
||||||
|
@ -55,7 +58,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
|
|
||||||
public PeopleView(PersonService personService) {
|
public PeopleView(PersonService personService) {
|
||||||
this.personService = personService;
|
this.personService = personService;
|
||||||
addClassNames("people-view");
|
addClassNames("expenses-view");
|
||||||
|
|
||||||
// Create UI
|
// Create UI
|
||||||
SplitLayout splitLayout = new SplitLayout();
|
SplitLayout splitLayout = new SplitLayout();
|
||||||
|
@ -71,7 +74,13 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
|
grid.addColumn(Person::getEmail).setHeader("Email").setSortable(true);
|
||||||
grid.addColumn(personService::calculateDebt).setHeader("Debt").setSortable(true);
|
grid.addColumn(personService::calculateDebt).setHeader("Debt").setSortable(true);
|
||||||
grid.addColumn(personService::calculateCredit).setHeader("Credit").setSortable(true);
|
grid.addColumn(personService::calculateCredit).setHeader("Credit").setSortable(true);
|
||||||
grid.addColumn(personService::calculateTotalExpenses).setHeader("Total Expenses value").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.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
grid.setItems(query -> personService.list(
|
grid.setItems(query -> personService.list(
|
||||||
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
|
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
|
||||||
|
@ -169,6 +178,10 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
firstName = new TextField("First Name");
|
firstName = new TextField("First Name");
|
||||||
lastName = new TextField("Last Name");
|
lastName = new TextField("Last Name");
|
||||||
email = new EmailField("Email");
|
email = new EmailField("Email");
|
||||||
|
|
||||||
|
// We set the maximum parallel columns to 1
|
||||||
|
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
|
||||||
|
|
||||||
formLayout.add(firstName, lastName, email);
|
formLayout.add(firstName, lastName, email);
|
||||||
editorDiv.add(formLayout);
|
editorDiv.add(formLayout);
|
||||||
createButtonLayout(editorLayoutDiv);
|
createButtonLayout(editorLayoutDiv);
|
||||||
|
@ -207,4 +220,18 @@ public class PeopleView extends Div implements BeforeEnterObserver {
|
||||||
binder.readBean(this.person);
|
binder.readBean(this.person);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Span createBadge(BigDecimal netBalance) {
|
||||||
|
Span badge = new Span();
|
||||||
|
if (netBalance.compareTo(BigDecimal.ZERO) < 0) {
|
||||||
|
badge.setText("Credit");
|
||||||
|
badge.getElement().getThemeList().add("badge success");
|
||||||
|
} else if (netBalance.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
badge.setText("Debit");
|
||||||
|
badge.getElement().getThemeList().add("badge error");
|
||||||
|
} else {
|
||||||
|
badge.setText("Clear");
|
||||||
|
badge.getElement().getThemeList().add("badge contrast");
|
||||||
}
|
}
|
||||||
|
return badge;
|
||||||
|
}}
|
|
@ -3,7 +3,7 @@ logging.level.org.atmosphere = warn
|
||||||
spring.mustache.check-template-location = false
|
spring.mustache.check-template-location = false
|
||||||
|
|
||||||
# Launch the default browser when starting the application in development mode
|
# Launch the default browser when starting the application in development mode
|
||||||
vaadin.launch-browser=true
|
#vaadin.launch-browser=true
|
||||||
# PostgreSQL configuration.
|
# PostgreSQL configuration.
|
||||||
spring.datasource.url = jdbc:postgresql://localhost:5432/munera_vaadin
|
spring.datasource.url = jdbc:postgresql://localhost:5432/munera_vaadin
|
||||||
spring.datasource.username = postgres
|
spring.datasource.username = postgres
|
||||||
|
|
Loading…
Reference in a new issue