diff --git a/src/main/java/com/application/munera/repositories/CategoryRepository.java b/src/main/java/com/application/munera/repositories/CategoryRepository.java index 450a564..e458d2c 100644 --- a/src/main/java/com/application/munera/repositories/CategoryRepository.java +++ b/src/main/java/com/application/munera/repositories/CategoryRepository.java @@ -1,9 +1,8 @@ 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, JpaSpecificationExecutor { +public interface CategoryRepository extends JpaRepository, JpaSpecificationExecutor { } diff --git a/src/main/java/com/application/munera/repositories/EventRepository.java b/src/main/java/com/application/munera/repositories/EventRepository.java new file mode 100644 index 0000000..895f65b --- /dev/null +++ b/src/main/java/com/application/munera/repositories/EventRepository.java @@ -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, JpaSpecificationExecutor { +} diff --git a/src/main/java/com/application/munera/services/EventService.java b/src/main/java/com/application/munera/services/EventService.java new file mode 100644 index 0000000..87858a8 --- /dev/null +++ b/src/main/java/com/application/munera/services/EventService.java @@ -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 findById(Long id) { + return eventRepository.findById(id); + } + + public List findAll() { + return eventRepository.findAll(); + } + + public void update(Event event) { + eventRepository.save(event); + } + + public void delete(Event event) { + eventRepository.delete(event); + } + + public Page list(Pageable pageable){ + return eventRepository.findAll(pageable); + } +} diff --git a/src/main/java/com/application/munera/views/MainLayout.java b/src/main/java/com/application/munera/views/MainLayout.java index 26e8878..5773992 100644 --- a/src/main/java/com/application/munera/views/MainLayout.java +++ b/src/main/java/com/application/munera/views/MainLayout.java @@ -1,6 +1,7 @@ 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; @@ -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("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; } diff --git a/src/main/java/com/application/munera/views/expenses/EventsView.java b/src/main/java/com/application/munera/views/expenses/EventsView.java new file mode 100644 index 0000000..cf69ace --- /dev/null +++ b/src/main/java/com/application/munera/views/expenses/EventsView.java @@ -0,0 +1,202 @@ +package com.application.munera.views.expenses; + +import com.application.munera.data.Event; +import com.application.munera.services.EventService; +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.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 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 binder; + + private Event event; + private final EventService eventService; + private TextField name; + private TextArea description; + + public EventsView(EventService eventService) { + this.eventService = eventService; + addClassNames("events-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 eventId = event.getRouteParameters().get(EVENT_ID).map(Long::parseLong); + if (eventId.isPresent()) { + Optional 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"); + formLayout.add(name, description); + 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); + } +}