diff --git a/src/main/java/com/application/munera/views/MainLayout.java b/src/main/java/com/application/munera/views/MainLayout.java index 76e67ec..78bd1d6 100644 --- a/src/main/java/com/application/munera/views/MainLayout.java +++ b/src/main/java/com/application/munera/views/MainLayout.java @@ -1,7 +1,6 @@ package com.application.munera.views; import com.application.munera.views.expenses.ExpensesView; -import com.application.munera.views.expenses.FormView; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.applayout.DrawerToggle; import com.vaadin.flow.component.html.Footer; @@ -52,7 +51,6 @@ public class MainLayout extends AppLayout { SideNav nav = new SideNav(); nav.addItem(new SideNavItem("Expenses", ExpensesView.class, LineAwesomeIcon.COLUMNS_SOLID.create())); - nav.addItem(new SideNavItem("Form", FormView.class, LineAwesomeIcon.COLUMNS_SOLID.create())); return nav; } diff --git a/src/main/java/com/application/munera/views/expenses/ExpensesView.java b/src/main/java/com/application/munera/views/expenses/ExpensesView.java index 55e248c..a5687f1 100644 --- a/src/main/java/com/application/munera/views/expenses/ExpensesView.java +++ b/src/main/java/com/application/munera/views/expenses/ExpensesView.java @@ -1,39 +1,82 @@ package com.application.munera.views.expenses; +import com.application.munera.data.Category; import com.application.munera.data.Expense; +import com.application.munera.data.PeriodUnit; +import com.application.munera.services.CategoryService; import com.application.munera.services.ExpenseService; 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.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.datepicker.DatePicker; +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.orderedlayout.VerticalLayout; +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.Notification.Position; +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.*; -import jakarta.annotation.security.PermitAll; -import org.springframework.beans.factory.annotation.Autowired; +import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; +import org.springframework.data.domain.PageRequest; +import org.springframework.orm.ObjectOptimisticLockingFailureException; -import java.util.List; +import java.util.Optional; @PageTitle("Expenses") -@Route(value = "/", layout = MainLayout.class) -@PermitAll -public class ExpensesView extends VerticalLayout { +@Route(value = "/:expenseID?/:action?(edit)", layout = MainLayout.class) +@RouteAlias(value = "", layout = MainLayout.class) +@Uses(Icon.class) +public class ExpensesView extends Div implements BeforeEnterObserver { + + private final String EXPENSE_ID = "ExpenseID"; + private final String SAMPLEPERSON_EDIT_ROUTE_TEMPLATE = "/%s/edit"; + + private final Grid grid = new Grid<>(Expense.class, false); + + private final Button cancel = new Button("Cancel"); + private final Button save = new Button("Save"); + + private final BeanValidationBinder binder; + + private Expense expense; private final ExpenseService expenseService; - private final Grid grid; + private final CategoryService categoryService; + private TextField name; + private TextField cost; + private ComboBox category; + private TextArea description; + private Checkbox isPeriodic; + private ComboBox periodUnit; + private TextField periodInterval; + private DatePicker date; - @Autowired - public ExpensesView(ExpenseService expenseService) { + public ExpensesView(ExpenseService expenseService, CategoryService categoryService) { this.expenseService = expenseService; - this.grid = new Grid<>(Expense.class, false); + this.categoryService = categoryService; + addClassNames("expenses-view"); - addClassName("expenses-view"); - setSizeFull(); - configureGrid(); - add(grid); - updateList(); - } + // Create UI + SplitLayout splitLayout = new SplitLayout(); - private void configureGrid() { - grid.addClassNames("expense-grid"); - grid.setSizeFull(); + createGridLayout(splitLayout); + createEditorLayout(splitLayout); + + add(splitLayout); + + // Configure Grid grid.addColumn(Expense::getName).setHeader("Name").setSortable(true); grid.addColumn(Expense::getCost).setHeader("Amount").setSortable(true); grid.addColumn(Expense::getCategory).setHeader("Category").setSortable(true); @@ -41,10 +84,131 @@ public class ExpensesView extends VerticalLayout { grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true); grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true); grid.getColumns().forEach(col -> col.setAutoWidth(true)); + + grid.setItems(query -> expenseService.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(SAMPLEPERSON_EDIT_ROUTE_TEMPLATE, event.getValue().getId())); + } else { + clearForm(); + UI.getCurrent().navigate(ExpensesView.class); + } + }); + + // Configure Form + binder = new BeanValidationBinder<>(Expense.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.expense == null) { + this.expense = new Expense(); + } + binder.writeBean(this.expense); + expenseService.update(this.expense); + clearForm(); + refreshGrid(); + Notification.show("Data updated"); + UI.getCurrent().navigate(ExpensesView.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(Position.MIDDLE); + n.addThemeVariants(NotificationVariant.LUMO_ERROR); + } catch (ValidationException validationException) { + Notification.show("Failed to update the data. Check again that all values are valid"); + } + }); } - private void updateList() { - List expenses = expenseService.findAll(); - grid.setItems(expenses); + @Override + public void beforeEnter(BeforeEnterEvent event) { + Optional expenseId = event.getRouteParameters().get(EXPENSE_ID).map(Long::parseLong); + if (expenseId.isPresent()) { + Optional samplePersonFromBackend = expenseService.get(expenseId.get()); + if (samplePersonFromBackend.isPresent()) { + populateForm(samplePersonFromBackend.get()); + } else { + Notification.show( + String.format("The requested samplePerson was not found, ID = %s", expenseId.get()), 3000, + Notification.Position.BOTTOM_START); + // when a row is selected but the data is no longer available, + // refresh grid + refreshGrid(); + event.forwardTo(ExpensesView.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"); + cost = new TextField("Cost"); + category = new ComboBox<>("Category"); + category.setItems(categoryService.findAll()); + category.setItemLabelGenerator(Category::getName); + description = new TextArea("Description"); + isPeriodic = new Checkbox("Is Periodic"); + periodUnit = new ComboBox<>("Period Unit"); + periodUnit.setItems(PeriodUnit.values()); + periodInterval = new TextField("Period Interval"); + date = new DatePicker("Date"); + + formLayout.add(name, cost, category, description, isPeriodic, periodUnit, periodInterval, date); + 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); + buttonLayout.add(save, 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(Expense value) { + this.expense = value; + binder.readBean(this.expense); + } } \ No newline at end of file diff --git a/src/main/java/com/application/munera/views/expenses/FormView.java b/src/main/java/com/application/munera/views/expenses/FormView.java deleted file mode 100644 index 285e895..0000000 --- a/src/main/java/com/application/munera/views/expenses/FormView.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.application.munera.views.expenses; - -import com.vaadin.flow.component.formlayout.FormLayout; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.textfield.PasswordField; -import com.vaadin.flow.router.Route; - -import java.awt.*; - -@Route("form") -public class FormView extends Div { - - public FormView() { - TextField firstName = new TextField("First name"); - TextField lastName = new TextField("Last name"); - TextField username = new TextField("Username"); - PasswordField password = new PasswordField("Password"); - PasswordField confirmPassword = new PasswordField("Confirm password"); - - FormLayout formLayout = new FormLayout(); - formLayout.add(password); - formLayout.setResponsiveSteps( - // Use one column by default - new com.vaadin.flow.component.formlayout.FormLayout.ResponsiveStep("0", 1), - // Use two columns, if layout's width exceeds 500px - new com.vaadin.flow.component.formlayout.FormLayout.ResponsiveStep("500px", 2)); - // Stretch the username field over 2 columns - formLayout.setColspan(password, 2); - - add(formLayout); - } - -} \ No newline at end of file