Added category view and editor.
This commit is contained in:
		
							parent
							
								
									77291ba724
								
							
						
					
					
						commit
						aaa1081ddf
					
				| 
						 | 
				
			
			@ -93,6 +93,8 @@ public class PerfinApp extends Application {
 | 
			
		|||
            router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
 | 
			
		||||
            router.map("vendors", PerfinApp.class.getResource("/vendors-view.fxml"));
 | 
			
		||||
            router.map("edit-vendor", PerfinApp.class.getResource("/edit-vendor.fxml"));
 | 
			
		||||
            router.map("categories", PerfinApp.class.getResource("/categories-view.fxml"));
 | 
			
		||||
            router.map("edit-category", PerfinApp.class.getResource("/edit-category.fxml"));
 | 
			
		||||
 | 
			
		||||
            // Help pages.
 | 
			
		||||
            helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
package com.andrewlalis.perfin.control;
 | 
			
		||||
 | 
			
		||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
 | 
			
		||||
import com.andrewlalis.perfin.data.TransactionCategoryRepository;
 | 
			
		||||
import com.andrewlalis.perfin.model.Profile;
 | 
			
		||||
import com.andrewlalis.perfin.view.BindingUtil;
 | 
			
		||||
import com.andrewlalis.perfin.view.component.CategoryTile;
 | 
			
		||||
import javafx.application.Platform;
 | 
			
		||||
import javafx.collections.FXCollections;
 | 
			
		||||
import javafx.collections.ObservableList;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
 | 
			
		||||
public class CategoriesViewController implements RouteSelectionListener {
 | 
			
		||||
    @FXML public VBox categoriesVBox;
 | 
			
		||||
    private final ObservableList<TransactionCategoryRepository.CategoryTreeNode> categoryTreeNodes = FXCollections.observableArrayList();
 | 
			
		||||
 | 
			
		||||
    @FXML public void initialize() {
 | 
			
		||||
        BindingUtil.mapContent(categoriesVBox.getChildren(), categoryTreeNodes, node -> new CategoryTile(node, this::refreshCategories));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onRouteSelected(Object context) {
 | 
			
		||||
        refreshCategories();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void refreshCategories() {
 | 
			
		||||
        Profile.getCurrent().dataSource().mapRepoAsync(
 | 
			
		||||
                TransactionCategoryRepository.class,
 | 
			
		||||
                TransactionCategoryRepository::findTree
 | 
			
		||||
        ).thenAccept(nodes -> Platform.runLater(() -> categoryTreeNodes.setAll(nodes)));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,108 @@
 | 
			
		|||
package com.andrewlalis.perfin.control;
 | 
			
		||||
 | 
			
		||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
 | 
			
		||||
import com.andrewlalis.perfin.data.TransactionCategoryRepository;
 | 
			
		||||
import com.andrewlalis.perfin.model.Profile;
 | 
			
		||||
import com.andrewlalis.perfin.model.TransactionCategory;
 | 
			
		||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
 | 
			
		||||
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.scene.control.Button;
 | 
			
		||||
import javafx.scene.control.ColorPicker;
 | 
			
		||||
import javafx.scene.control.TextField;
 | 
			
		||||
import javafx.scene.paint.Color;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
 | 
			
		||||
import static com.andrewlalis.perfin.PerfinApp.router;
 | 
			
		||||
 | 
			
		||||
public class EditCategoryController implements RouteSelectionListener {
 | 
			
		||||
    public record CategoryRouteContext(TransactionCategory category) implements RouteContext {}
 | 
			
		||||
    public record AddSubcategoryRouteContext(TransactionCategory parent) implements RouteContext {}
 | 
			
		||||
    private sealed interface RouteContext permits AddSubcategoryRouteContext, CategoryRouteContext {}
 | 
			
		||||
 | 
			
		||||
    private TransactionCategory category;
 | 
			
		||||
    private TransactionCategory parent;
 | 
			
		||||
 | 
			
		||||
    @FXML public TextField nameField;
 | 
			
		||||
    @FXML public ColorPicker colorPicker;
 | 
			
		||||
 | 
			
		||||
    @FXML public Button saveButton;
 | 
			
		||||
 | 
			
		||||
    @FXML public void initialize() {
 | 
			
		||||
        var nameValid = new ValidationApplier<>(new PredicateValidator<String>()
 | 
			
		||||
                .addTerminalPredicate(s -> s != null && !s.isBlank(), "Name is required.")
 | 
			
		||||
                .addPredicate(s -> s.strip().length() <= TransactionCategory.NAME_MAX_LENGTH, "Name is too long.")
 | 
			
		||||
                .addAsyncPredicate(
 | 
			
		||||
                        s -> {
 | 
			
		||||
                            if (Profile.getCurrent() == null) return CompletableFuture.completedFuture(false);
 | 
			
		||||
                            return Profile.getCurrent().dataSource().mapRepoAsync(
 | 
			
		||||
                                    TransactionCategoryRepository.class,
 | 
			
		||||
                                    repo -> {
 | 
			
		||||
                                        var categoryByName = repo.findByName(s).orElse(null);
 | 
			
		||||
                                        if (this.category != null) {
 | 
			
		||||
                                            return this.category.equals(categoryByName) || categoryByName == null;
 | 
			
		||||
                                        }
 | 
			
		||||
                                        return categoryByName == null;
 | 
			
		||||
                                    }
 | 
			
		||||
                            );
 | 
			
		||||
                        },
 | 
			
		||||
                        "Category with this name already exists."
 | 
			
		||||
                )
 | 
			
		||||
        ).validatedInitially().attachToTextField(nameField);
 | 
			
		||||
 | 
			
		||||
        saveButton.disableProperty().bind(nameValid.not());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onRouteSelected(Object context) {
 | 
			
		||||
        this.category = null;
 | 
			
		||||
        this.parent = null;
 | 
			
		||||
        if (context instanceof RouteContext ctx) {
 | 
			
		||||
            switch (ctx) {
 | 
			
		||||
                case CategoryRouteContext(var cat):
 | 
			
		||||
                    this.category = cat;
 | 
			
		||||
                    nameField.setText(cat.getName());
 | 
			
		||||
                    colorPicker.setValue(cat.getColor());
 | 
			
		||||
                    break;
 | 
			
		||||
                case AddSubcategoryRouteContext(var par):
 | 
			
		||||
                    this.parent = par;
 | 
			
		||||
                    nameField.setText(null);
 | 
			
		||||
                    colorPicker.setValue(parent.getColor());
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            nameField.setText(null);
 | 
			
		||||
            colorPicker.setValue(Color.WHITE);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @FXML public void save() {
 | 
			
		||||
        final String name = nameField.getText().strip();
 | 
			
		||||
        final Color color = colorPicker.getValue();
 | 
			
		||||
        if (this.category == null && this.parent == null) {
 | 
			
		||||
            // New top-level category.
 | 
			
		||||
            Profile.getCurrent().dataSource().useRepo(
 | 
			
		||||
                    TransactionCategoryRepository.class,
 | 
			
		||||
                    repo -> repo.insert(name, color)
 | 
			
		||||
            );
 | 
			
		||||
        } else if (this.category == null) {
 | 
			
		||||
            // New subcategory.
 | 
			
		||||
            Profile.getCurrent().dataSource().useRepo(
 | 
			
		||||
                    TransactionCategoryRepository.class,
 | 
			
		||||
                    repo -> repo.insert(parent.id, name, color)
 | 
			
		||||
            );
 | 
			
		||||
        } else if (this.parent == null) {
 | 
			
		||||
            // Save edits to an existing category.
 | 
			
		||||
            Profile.getCurrent().dataSource().useRepo(
 | 
			
		||||
                    TransactionCategoryRepository.class,
 | 
			
		||||
                    repo -> repo.update(category.id, name, color)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        router.replace("categories");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @FXML public void cancel() {
 | 
			
		||||
        router.navigateBackAndClear();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +62,9 @@ public class EditTransactionController implements RouteSelectionListener {
 | 
			
		|||
    @FXML public ComboBox<String> vendorComboBox;
 | 
			
		||||
    @FXML public Hyperlink vendorsHyperlink;
 | 
			
		||||
    @FXML public ComboBox<String> categoryComboBox;
 | 
			
		||||
    @FXML public Hyperlink categoriesHyperlink;
 | 
			
		||||
    @FXML public ComboBox<String> tagsComboBox;
 | 
			
		||||
    @FXML public Hyperlink tagsHyperlink;
 | 
			
		||||
    @FXML public Button addTagButton;
 | 
			
		||||
    @FXML public VBox tagsVBox;
 | 
			
		||||
    private final ObservableList<String> selectedTags = FXCollections.observableArrayList();
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +94,8 @@ public class EditTransactionController implements RouteSelectionListener {
 | 
			
		|||
        initializeTagSelectionUi();
 | 
			
		||||
        // Setup hyperlinks.
 | 
			
		||||
        vendorsHyperlink.setOnAction(event -> router.navigate("vendors"));
 | 
			
		||||
        categoriesHyperlink.setOnAction(event -> router.navigate("categories"));
 | 
			
		||||
//        tagsHyperlink.setOnAction(event -> router.navigate("tags"));
 | 
			
		||||
 | 
			
		||||
        var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
 | 
			
		||||
        saveButton.disableProperty().bind(formValid.not());
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,15 +5,11 @@ import com.andrewlalis.perfin.data.TransactionVendorRepository;
 | 
			
		|||
import com.andrewlalis.perfin.model.Profile;
 | 
			
		||||
import com.andrewlalis.perfin.model.TransactionVendor;
 | 
			
		||||
import com.andrewlalis.perfin.view.BindingUtil;
 | 
			
		||||
import com.andrewlalis.perfin.view.component.VendorTile;
 | 
			
		||||
import javafx.application.Platform;
 | 
			
		||||
import javafx.collections.FXCollections;
 | 
			
		||||
import javafx.collections.ObservableList;
 | 
			
		||||
import javafx.fxml.FXML;
 | 
			
		||||
import javafx.geometry.Pos;
 | 
			
		||||
import javafx.scene.Node;
 | 
			
		||||
import javafx.scene.control.Button;
 | 
			
		||||
import javafx.scene.control.Label;
 | 
			
		||||
import javafx.scene.layout.BorderPane;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
| 
						 | 
				
			
			@ -25,36 +21,7 @@ public class VendorsViewController implements RouteSelectionListener {
 | 
			
		|||
    private final ObservableList<TransactionVendor> vendors = FXCollections.observableArrayList();
 | 
			
		||||
 | 
			
		||||
    @FXML public void initialize() {
 | 
			
		||||
        BindingUtil.mapContent(vendorsVBox.getChildren(), vendors, this::buildVendorTile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Node buildVendorTile(TransactionVendor transactionVendor) {
 | 
			
		||||
        BorderPane pane = new BorderPane();
 | 
			
		||||
        pane.getStyleClass().addAll("tile", "std-spacing");
 | 
			
		||||
        pane.setOnMouseClicked(event -> router.navigate("edit-vendor", transactionVendor));
 | 
			
		||||
 | 
			
		||||
        Label nameLabel = new Label(transactionVendor.getName());
 | 
			
		||||
        nameLabel.getStyleClass().addAll("bold-text");
 | 
			
		||||
        Label descriptionLabel = new Label(transactionVendor.getDescription());
 | 
			
		||||
        descriptionLabel.setWrapText(true);
 | 
			
		||||
        VBox contentVBox = new VBox(nameLabel, descriptionLabel);
 | 
			
		||||
        contentVBox.getStyleClass().addAll("std-spacing");
 | 
			
		||||
        pane.setCenter(contentVBox);
 | 
			
		||||
        BorderPane.setAlignment(contentVBox, Pos.TOP_LEFT);
 | 
			
		||||
 | 
			
		||||
        Button removeButton = new Button("Remove");
 | 
			
		||||
        removeButton.setOnAction(event -> {
 | 
			
		||||
            boolean confirm = Popups.confirm(removeButton, "Are you sure you want to remove this vendor? Any transactions with assigned to this vendor will have their vendor field cleared. This cannot be undone.");
 | 
			
		||||
            if (confirm) {
 | 
			
		||||
                Profile.getCurrent().dataSource().useRepo(TransactionVendorRepository.class, repo -> {
 | 
			
		||||
                    repo.deleteById(transactionVendor.id);
 | 
			
		||||
                });
 | 
			
		||||
                refreshVendors();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        pane.setRight(removeButton);
 | 
			
		||||
 | 
			
		||||
        return pane;
 | 
			
		||||
        BindingUtil.mapContent(vendorsVBox.getChildren(), vendors, vendor -> new VendorTile(vendor, this::refreshVendors));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,5 +13,9 @@ public interface TransactionCategoryRepository extends Repository, AutoCloseable
 | 
			
		|||
    List<TransactionCategory> findAll();
 | 
			
		||||
    long insert(long parentId, String name, Color color);
 | 
			
		||||
    long insert(String name, Color color);
 | 
			
		||||
    void update(long id, String name, Color color);
 | 
			
		||||
    void deleteById(long id);
 | 
			
		||||
 | 
			
		||||
    record CategoryTreeNode(TransactionCategory category, List<CategoryTreeNode> children){}
 | 
			
		||||
    List<CategoryTreeNode> findTree();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import javafx.scene.paint.Color;
 | 
			
		|||
import java.sql.Connection;
 | 
			
		||||
import java.sql.ResultSet;
 | 
			
		||||
import java.sql.SQLException;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -69,11 +70,62 @@ public record JdbcTransactionCategoryRepository(Connection conn) implements Tran
 | 
			
		|||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void update(long id, String name, Color color) {
 | 
			
		||||
        DbUtil.doTransaction(conn, () -> {
 | 
			
		||||
            TransactionCategory category = findById(id).orElseThrow();
 | 
			
		||||
            if (!category.getName().equals(name)) {
 | 
			
		||||
                DbUtil.updateOne(
 | 
			
		||||
                        conn,
 | 
			
		||||
                        "UPDATE transaction_category SET name = ? WHERE id = ?",
 | 
			
		||||
                        name,
 | 
			
		||||
                        id
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            if (!category.getColor().equals(color)) {
 | 
			
		||||
                DbUtil.updateOne(
 | 
			
		||||
                        conn,
 | 
			
		||||
                        "UPDATE transaction_category SET color = ? WHERE id = ?",
 | 
			
		||||
                        ColorUtil.toHex(color),
 | 
			
		||||
                        id
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void deleteById(long id) {
 | 
			
		||||
        DbUtil.updateOne(conn, "DELETE FROM transaction_category WHERE id = ?", id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public List<CategoryTreeNode> findTree() {
 | 
			
		||||
        List<TransactionCategory> rootCategories = DbUtil.findAll(
 | 
			
		||||
                conn,
 | 
			
		||||
                "SELECT * FROM transaction_category WHERE parent_id IS NULL ORDER BY name ASC",
 | 
			
		||||
                JdbcTransactionCategoryRepository::parseCategory
 | 
			
		||||
        );
 | 
			
		||||
        List<CategoryTreeNode> rootNodes = new ArrayList<>(rootCategories.size());
 | 
			
		||||
        for (var category : rootCategories) {
 | 
			
		||||
            rootNodes.add(findTreeRecursive(category));
 | 
			
		||||
        }
 | 
			
		||||
        return rootNodes;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private CategoryTreeNode findTreeRecursive(TransactionCategory root) {
 | 
			
		||||
        CategoryTreeNode node = new CategoryTreeNode(root, new ArrayList<>());
 | 
			
		||||
        List<TransactionCategory> childCategories = DbUtil.findAll(
 | 
			
		||||
                conn,
 | 
			
		||||
                "SELECT * FROM transaction_category WHERE parent_id = ? ORDER BY name ASC",
 | 
			
		||||
                List.of(root.id),
 | 
			
		||||
                JdbcTransactionCategoryRepository::parseCategory
 | 
			
		||||
        );
 | 
			
		||||
        for (var childCategory : childCategories) {
 | 
			
		||||
            node.children().add(findTreeRecursive(childCategory));
 | 
			
		||||
        }
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void close() throws Exception {
 | 
			
		||||
        conn.close();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
package com.andrewlalis.perfin.view.component;
 | 
			
		||||
 | 
			
		||||
import com.andrewlalis.perfin.control.EditCategoryController;
 | 
			
		||||
import com.andrewlalis.perfin.control.Popups;
 | 
			
		||||
import com.andrewlalis.perfin.data.TransactionCategoryRepository;
 | 
			
		||||
import com.andrewlalis.perfin.model.Profile;
 | 
			
		||||
import javafx.scene.control.Button;
 | 
			
		||||
import javafx.scene.control.Label;
 | 
			
		||||
import javafx.scene.layout.BorderPane;
 | 
			
		||||
import javafx.scene.layout.HBox;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
import javafx.scene.shape.Circle;
 | 
			
		||||
 | 
			
		||||
import static com.andrewlalis.perfin.PerfinApp.router;
 | 
			
		||||
 | 
			
		||||
public class CategoryTile extends VBox {
 | 
			
		||||
    public CategoryTile(
 | 
			
		||||
            TransactionCategoryRepository.CategoryTreeNode treeNode,
 | 
			
		||||
            Runnable categoriesRefresh
 | 
			
		||||
    ) {
 | 
			
		||||
        this.getStyleClass().addAll("tile", "hand-cursor");
 | 
			
		||||
        this.setStyle("-fx-border-width: 1px; -fx-border-color: grey;");
 | 
			
		||||
        this.setOnMouseClicked(event -> {
 | 
			
		||||
            event.consume();
 | 
			
		||||
            router.navigate(
 | 
			
		||||
                    "edit-category",
 | 
			
		||||
                    new EditCategoryController.CategoryRouteContext(treeNode.category())
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        BorderPane borderPane = new BorderPane();
 | 
			
		||||
        borderPane.getStyleClass().addAll("std-padding");
 | 
			
		||||
        Label nameLabel = new Label(treeNode.category().getName());
 | 
			
		||||
        nameLabel.getStyleClass().addAll("bold-text");
 | 
			
		||||
        Circle colorCircle = new Circle(10, treeNode.category().getColor());
 | 
			
		||||
        HBox contentBox = new HBox(colorCircle, nameLabel);
 | 
			
		||||
        contentBox.getStyleClass().addAll("std-spacing");
 | 
			
		||||
        borderPane.setLeft(contentBox);
 | 
			
		||||
 | 
			
		||||
        Button addChildButton = new Button("Add Subcategory");
 | 
			
		||||
        addChildButton.setOnAction(event -> router.navigate(
 | 
			
		||||
                "edit-category",
 | 
			
		||||
                new EditCategoryController.AddSubcategoryRouteContext(treeNode.category())
 | 
			
		||||
        ));
 | 
			
		||||
        Button removeButton = new Button("Remove");
 | 
			
		||||
        removeButton.setOnAction(event -> {
 | 
			
		||||
            boolean confirm = Popups.confirm(removeButton, "Are you sure you want to remove this category? It will permanently remove the category from all linked transactions, and all subcategories will also be removed. This cannot be undone.");
 | 
			
		||||
            if (confirm) {
 | 
			
		||||
                Profile.getCurrent().dataSource().useRepo(
 | 
			
		||||
                        TransactionCategoryRepository.class,
 | 
			
		||||
                        repo -> repo.deleteById(treeNode.category().id)
 | 
			
		||||
                );
 | 
			
		||||
                categoriesRefresh.run();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        HBox buttonsBox = new HBox(addChildButton, removeButton);
 | 
			
		||||
        buttonsBox.getStyleClass().addAll("std-spacing");
 | 
			
		||||
        borderPane.setRight(buttonsBox);
 | 
			
		||||
 | 
			
		||||
        this.getChildren().add(borderPane);
 | 
			
		||||
        for (var child : treeNode.children()) {
 | 
			
		||||
            this.getChildren().add(new CategoryTile(child, categoriesRefresh));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,46 @@
 | 
			
		|||
package com.andrewlalis.perfin.view.component;
 | 
			
		||||
 | 
			
		||||
import com.andrewlalis.perfin.control.Popups;
 | 
			
		||||
import com.andrewlalis.perfin.data.TransactionVendorRepository;
 | 
			
		||||
import com.andrewlalis.perfin.model.Profile;
 | 
			
		||||
import com.andrewlalis.perfin.model.TransactionVendor;
 | 
			
		||||
import javafx.geometry.Pos;
 | 
			
		||||
import javafx.scene.control.Button;
 | 
			
		||||
import javafx.scene.control.Label;
 | 
			
		||||
import javafx.scene.layout.BorderPane;
 | 
			
		||||
import javafx.scene.layout.VBox;
 | 
			
		||||
 | 
			
		||||
import static com.andrewlalis.perfin.PerfinApp.router;
 | 
			
		||||
 | 
			
		||||
public class VendorTile extends BorderPane {
 | 
			
		||||
    public VendorTile(TransactionVendor vendor, Runnable vendorRefresh) {
 | 
			
		||||
        this.getStyleClass().addAll("tile", "std-spacing", "hand-cursor");
 | 
			
		||||
        this.setOnMouseClicked(event -> router.navigate("edit-vendor", vendor));
 | 
			
		||||
 | 
			
		||||
        Label nameLabel = new Label(vendor.getName());
 | 
			
		||||
        nameLabel.getStyleClass().addAll("bold-text");
 | 
			
		||||
        Label descriptionLabel = new Label(vendor.getDescription());
 | 
			
		||||
        descriptionLabel.setWrapText(true);
 | 
			
		||||
        VBox contentVBox = new VBox(nameLabel, descriptionLabel);
 | 
			
		||||
        contentVBox.getStyleClass().addAll("std-spacing");
 | 
			
		||||
        this.setCenter(contentVBox);
 | 
			
		||||
        BorderPane.setAlignment(contentVBox, Pos.TOP_LEFT);
 | 
			
		||||
 | 
			
		||||
        this.setRight(getRemoveButton(vendor, vendorRefresh));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Button getRemoveButton(TransactionVendor transactionVendor, Runnable vendorRefresh) {
 | 
			
		||||
        Button removeButton = new Button("Remove");
 | 
			
		||||
        removeButton.setOnAction(event -> {
 | 
			
		||||
            boolean confirm = Popups.confirm(removeButton, "Are you sure you want to remove this vendor? Any transactions assigned to this vendor will have their vendor field cleared. This cannot be undone.");
 | 
			
		||||
            if (confirm) {
 | 
			
		||||
                Profile.getCurrent().dataSource().useRepo(
 | 
			
		||||
                        TransactionVendorRepository.class,
 | 
			
		||||
                        repo -> repo.deleteById(transactionVendor.id)
 | 
			
		||||
                );
 | 
			
		||||
                vendorRefresh.run();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return removeButton;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
 | 
			
		||||
<?import javafx.scene.layout.BorderPane?>
 | 
			
		||||
<?import javafx.scene.control.Label?>
 | 
			
		||||
<?import javafx.scene.layout.VBox?>
 | 
			
		||||
<?import javafx.scene.layout.HBox?>
 | 
			
		||||
<?import javafx.scene.control.Button?>
 | 
			
		||||
<?import javafx.scene.control.ScrollPane?>
 | 
			
		||||
<BorderPane xmlns="http://javafx.com/javafx"
 | 
			
		||||
            xmlns:fx="http://javafx.com/fxml"
 | 
			
		||||
            fx:controller="com.andrewlalis.perfin.control.CategoriesViewController"
 | 
			
		||||
>
 | 
			
		||||
    <top>
 | 
			
		||||
        <Label text="Transaction Categories" styleClass="large-font,bold-text,std-padding"/>
 | 
			
		||||
    </top>
 | 
			
		||||
    <center>
 | 
			
		||||
        <VBox>
 | 
			
		||||
            <HBox styleClass="std-padding, std-spacing" VBox.vgrow="NEVER">
 | 
			
		||||
                <Button text="Add Category"/>
 | 
			
		||||
            </HBox>
 | 
			
		||||
            <ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
 | 
			
		||||
                <VBox fx:id="categoriesVBox" styleClass="tile-container"/>
 | 
			
		||||
            </ScrollPane>
 | 
			
		||||
        </VBox>
 | 
			
		||||
    </center>
 | 
			
		||||
</BorderPane>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
 | 
			
		||||
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
 | 
			
		||||
<?import javafx.scene.control.*?>
 | 
			
		||||
<?import javafx.scene.layout.*?>
 | 
			
		||||
<BorderPane xmlns="http://javafx.com/javafx"
 | 
			
		||||
            xmlns:fx="http://javafx.com/fxml"
 | 
			
		||||
            fx:controller="com.andrewlalis.perfin.control.EditCategoryController"
 | 
			
		||||
>
 | 
			
		||||
    <top>
 | 
			
		||||
        <Label text="Edit Transaction Category" styleClass="bold-text,large-font,std-padding"/>
 | 
			
		||||
    </top>
 | 
			
		||||
    <center>
 | 
			
		||||
        <VBox>
 | 
			
		||||
            <PropertiesPane hgap="5" vgap="5" styleClass="std-padding" maxWidth="500">
 | 
			
		||||
                <columnConstraints>
 | 
			
		||||
                    <ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
 | 
			
		||||
                    <ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
 | 
			
		||||
                </columnConstraints>
 | 
			
		||||
 | 
			
		||||
                <Label text="Name" labelFor="${nameField}"/>
 | 
			
		||||
                <TextField fx:id="nameField"/>
 | 
			
		||||
 | 
			
		||||
                <Label text="Color" labelFor="${colorPicker}"/>
 | 
			
		||||
                <ColorPicker fx:id="colorPicker"/>
 | 
			
		||||
            </PropertiesPane>
 | 
			
		||||
            <HBox styleClass="std-padding, std-spacing">
 | 
			
		||||
                <Button text="Save" fx:id="saveButton" onAction="#save"/>
 | 
			
		||||
                <Button text="Cancel" onAction="#cancel"/>
 | 
			
		||||
            </HBox>
 | 
			
		||||
        </VBox>
 | 
			
		||||
    </center>
 | 
			
		||||
</BorderPane>
 | 
			
		||||
| 
						 | 
				
			
			@ -68,13 +68,13 @@
 | 
			
		|||
 | 
			
		||||
                    <VBox>
 | 
			
		||||
                        <Label text="Category" labelFor="${categoryComboBox}" styleClass="bold-text"/>
 | 
			
		||||
                        <Hyperlink text="Manage categories" styleClass="small-font"/>
 | 
			
		||||
                        <Hyperlink fx:id="categoriesHyperlink" text="Manage categories" styleClass="small-font"/>
 | 
			
		||||
                    </VBox>
 | 
			
		||||
                    <ComboBox fx:id="categoryComboBox" editable="true" maxWidth="Infinity"/>
 | 
			
		||||
 | 
			
		||||
                    <VBox>
 | 
			
		||||
                        <Label text="Tags" labelFor="${tagsComboBox}" styleClass="bold-text"/>
 | 
			
		||||
                        <Hyperlink text="Manage tags" styleClass="small-font"/>
 | 
			
		||||
                        <Hyperlink fx:id="tagsHyperlink" text="Manage tags" styleClass="small-font"/>
 | 
			
		||||
                    </VBox>
 | 
			
		||||
                    <VBox maxWidth="Infinity">
 | 
			
		||||
                        <HBox styleClass="std-spacing">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,10 +3,7 @@
 | 
			
		|||
<?import javafx.scene.control.Button?>
 | 
			
		||||
<?import javafx.scene.control.Label?>
 | 
			
		||||
<?import javafx.scene.control.ScrollPane?>
 | 
			
		||||
<?import javafx.scene.layout.BorderPane?>
 | 
			
		||||
<?import javafx.scene.layout.HBox?>
 | 
			
		||||
<?import javafx.scene.layout.VBox?>
 | 
			
		||||
<?import javafx.scene.text.Text?>
 | 
			
		||||
<?import javafx.scene.layout.*?>
 | 
			
		||||
<BorderPane xmlns="http://javafx.com/javafx"
 | 
			
		||||
            xmlns:fx="http://javafx.com/fxml"
 | 
			
		||||
            fx:controller="com.andrewlalis.perfin.control.VendorsViewController"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue