Compare commits

..

No commits in common. "3595da20044785a4e01e3c7d7991d1db920fcb28" and "18680a0f90aaf3101df35b39a9677de49d57dbc2" have entirely different histories.

14 changed files with 27 additions and 380 deletions

View file

@ -1,23 +0,0 @@
<!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>

View file

@ -8,13 +8,14 @@ import lombok.Setter;
import java.util.Set;
@Entity
@Getter
@Setter
@Getter
@Table(name = "events")
public class Event {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Id", unique = true, nullable = false)
private Long id;
@Size(max = 100)

View file

@ -57,10 +57,6 @@ public class Expense {
inverseJoinColumns = @JoinColumn(name = "people_id"))
private Set<Person> debtors;
@ManyToOne
@JoinColumn(name = "EventId")
private Event event;
@Column(name = "Date", nullable = false, columnDefinition = "DATE DEFAULT CURRENT_DATE")
private LocalDate date;
}

View file

@ -7,7 +7,6 @@ import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.Set;
@Entity
@ -47,24 +46,5 @@ public class Person {
private Set<Expense> debtorExpenses;
@ManyToMany(mappedBy = "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);
}
}
private Set<Event> participants;
}

View file

@ -1,8 +1,9 @@
package com.application.munera.repositories;
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.JpaSpecificationExecutor;
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Category> {
public interface CategoryRepository extends JpaRepository<Category, Long>, JpaSpecificationExecutor<Expense> {
}

View file

@ -1,8 +0,0 @@
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> {
}

View file

@ -1,40 +0,0 @@
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);
}
}

View file

@ -9,7 +9,7 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@Service
@ -27,7 +27,7 @@ public class PersonService {
return personRepository.findById(id);
}
public Collection<Person> findAll() {
public List<Person> 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);
}
public BigDecimal calculateNetBalance(final Person person) {
public BigDecimal calculateTotalExpenses(final Person person) {
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);
return credit.subtract(debit);
return credit.add(debit);
}
}

View file

@ -1,7 +1,6 @@
package com.application.munera.views;
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.PeopleView;
import com.vaadin.flow.component.applayout.AppLayout;
@ -56,7 +55,6 @@ public class MainLayout extends AppLayout {
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("People", PeopleView.class, LineAwesomeIcon.USER.create()));
nav.addItem(new SideNavItem("Events", EventsView.class, LineAwesomeIcon.BANDCAMP.create()));
return nav;
}

View file

@ -51,7 +51,7 @@ public class CategoriesView extends Div implements BeforeEnterObserver {
public CategoriesView(CategoryService categoryService) {
this.categoryService = categoryService;
addClassNames("expenses-view");
addClassNames("categories-view");
// Create UI
SplitLayout splitLayout = new SplitLayout();

View file

@ -1,211 +0,0 @@
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);
}
}

View file

@ -1,8 +1,9 @@
package com.application.munera.views.expenses;
import com.application.munera.data.*;
import com.application.munera.data.Category;
import com.application.munera.data.Expense;
import com.application.munera.data.PeriodUnit;
import com.application.munera.data.Person;
import com.application.munera.services.CategoryService;
import com.application.munera.services.EventService;
import com.application.munera.services.ExpenseService;
import com.application.munera.services.PersonService;
import com.application.munera.views.MainLayout;
@ -34,7 +35,8 @@ import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.util.*;
import java.util.Objects;
import java.util.Optional;
@PageTitle("Expenses")
@Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class)
@ -57,7 +59,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
private final ExpenseService expenseService;
private final CategoryService categoryService;
private final PersonService personService;
private final EventService eventService;
private TextField name;
private TextField cost;
private ComboBox<Category> category;
@ -68,12 +69,11 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
private DatePicker date;
private MultiSelectComboBox<Person> creditors;
private MultiSelectComboBox<Person> debtors;
private ComboBox<Event> event;
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService, EventService eventService) {
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService) {
this.expenseService = expenseService;
this.categoryService = categoryService;
this.personService = personService;
this.eventService = eventService;
addClassNames("expenses-view");
// Create UI
@ -91,7 +91,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
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.getColumns().forEach(col -> col.setAutoWidth(true));
grid.setItems(query -> expenseService.list(
@ -116,9 +115,9 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
binder.bindInstanceFields(this);
// We set initial value of isPeriodic to true and show period fields
isPeriodic.setValue(false);
periodUnit.setVisible(false);
periodInterval.setVisible(false);
isPeriodic.setValue(true);
periodUnit.setVisible(true);
periodInterval.setVisible(true);
// We show the periodic fields only when the isPeriodic boolean is true
isPeriodic.addValueChangeListener(event -> {
@ -133,22 +132,6 @@ 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 -> {
clearForm();
refreshGrid();
@ -232,9 +215,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
creditors = new MultiSelectComboBox<>("Creditors");
creditors.setItems(personService.findAll());
creditors.setItemLabelGenerator(Person::getFirstName);
event = new ComboBox<>("Event");
event.setItems(eventService.findAll());
event.setItemLabelGenerator(Event::getName);
debtors = new MultiSelectComboBox<>("Debtors");
debtors.setItems(personService.findAll());
debtors.setItemLabelGenerator(Person::getFirstName);
@ -246,7 +226,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
? "var(--lumo-primary-text-color)"
: "var(--lumo-disabled-text-color)");
formLayout.add(name, cost, category, description, isPeriodic, periodUnit, periodInterval, date, creditors, debtors, event);
formLayout.add(name, cost, category, description, isPeriodic, periodUnit, periodInterval, date, creditors, debtors);
grid.addColumn(isPeriodicRenderer).setHeader("Periodic").setAutoWidth(true);
editorDiv.add(formLayout);
createButtonLayout(editorLayoutDiv);

View file

@ -11,7 +11,6 @@ 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;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.Notification.Position;
@ -22,7 +21,6 @@ import com.vaadin.flow.component.textfield.EmailField;
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.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
@ -31,7 +29,6 @@ import com.vaadin.flow.spring.data.VaadinSpringDataHelpers;
import org.springframework.data.domain.PageRequest;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.math.BigDecimal;
import java.util.Optional;
@PageTitle("People")
@ -58,7 +55,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
public PeopleView(PersonService personService) {
this.personService = personService;
addClassNames("expenses-view");
addClassNames("people-view");
// Create UI
SplitLayout splitLayout = new SplitLayout();
@ -74,13 +71,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
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.addColumn(personService::calculateTotalExpenses).setHeader("Total Expenses value").setSortable(true);
grid.getColumns().forEach(col -> col.setAutoWidth(true));
grid.setItems(query -> personService.list(
PageRequest.of(query.getPage(), query.getPageSize(), VaadinSpringDataHelpers.toSpringDataSort(query)))
@ -178,10 +169,6 @@ public class PeopleView extends Div implements BeforeEnterObserver {
firstName = new TextField("First Name");
lastName = new TextField("Last Name");
email = new EmailField("Email");
// We set the maximum parallel columns to 1
formLayout.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
formLayout.add(firstName, lastName, email);
editorDiv.add(formLayout);
createButtonLayout(editorLayoutDiv);
@ -220,18 +207,4 @@ public class PeopleView extends Div implements BeforeEnterObserver {
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;
}}
}

View file

@ -3,7 +3,7 @@ logging.level.org.atmosphere = warn
spring.mustache.check-template-location = false
# Launch the default browser when starting the application in development mode
#vaadin.launch-browser=true
vaadin.launch-browser=true
# PostgreSQL configuration.
spring.datasource.url = jdbc:postgresql://localhost:5432/munera_vaadin
spring.datasource.username = postgres