diff --git a/src/main/java/com/andrewlalis/perfin/PerfinApp.java b/src/main/java/com/andrewlalis/perfin/PerfinApp.java index ed0d8da..0ec8083 100644 --- a/src/main/java/com/andrewlalis/perfin/PerfinApp.java +++ b/src/main/java/com/andrewlalis/perfin/PerfinApp.java @@ -95,6 +95,7 @@ public class PerfinApp extends Application { 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")); + router.map("tags", PerfinApp.class.getResource("/tags-view.fxml")); // Help pages. helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml")); diff --git a/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java b/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java index abc613f..9697e76 100644 --- a/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/CategoriesViewController.java @@ -11,6 +11,8 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.layout.VBox; +import static com.andrewlalis.perfin.PerfinApp.router; + public class CategoriesViewController implements RouteSelectionListener { @FXML public VBox categoriesVBox; private final ObservableList categoryTreeNodes = FXCollections.observableArrayList(); @@ -24,6 +26,10 @@ public class CategoriesViewController implements RouteSelectionListener { refreshCategories(); } + @FXML public void addCategory() { + router.navigate("edit-category"); + } + private void refreshCategories() { Profile.getCurrent().dataSource().mapRepoAsync( TransactionCategoryRepository.class, diff --git a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java index ab3052f..d6dc755 100644 --- a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java +++ b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java @@ -92,10 +92,10 @@ public class EditTransactionController implements RouteSelectionListener { ).validatedInitially().attach(descriptionField, descriptionField.textProperty()); var linkedAccountsValid = initializeLinkedAccountsValidationUi(); initializeTagSelectionUi(); - // Setup hyperlinks. + vendorsHyperlink.setOnAction(event -> router.navigate("vendors")); categoriesHyperlink.setOnAction(event -> router.navigate("categories")); -// tagsHyperlink.setOnAction(event -> router.navigate("tags")); + tagsHyperlink.setOnAction(event -> router.navigate("tags")); var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid); saveButton.disableProperty().bind(formValid.not()); diff --git a/src/main/java/com/andrewlalis/perfin/control/TagsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TagsViewController.java new file mode 100644 index 0000000..2f0c569 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/control/TagsViewController.java @@ -0,0 +1,64 @@ +package com.andrewlalis.perfin.control; + +import com.andrewlalis.javafx_scene_router.RouteSelectionListener; +import com.andrewlalis.perfin.data.TransactionRepository; +import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.view.BindingUtil; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; + +public class TagsViewController implements RouteSelectionListener { + @FXML public VBox tagsVBox; + private final ObservableList tags = FXCollections.observableArrayList(); + + @FXML public void initialize() { + BindingUtil.mapContent(tagsVBox.getChildren(), tags, this::buildTagTile); + } + + @Override + public void onRouteSelected(Object context) { + refreshTags(); + } + + private void refreshTags() { + Profile.getCurrent().dataSource().mapRepoAsync( + TransactionRepository.class, + TransactionRepository::findAllTags + ).thenAccept(strings -> Platform.runLater(() -> tags.setAll(strings))); + } + + private Node buildTagTile(String name) { + BorderPane tile = new BorderPane(); + tile.getStyleClass().addAll("tile"); + Label nameLabel = new Label(name); + nameLabel.getStyleClass().addAll("bold-text"); + Label usagesLabel = new Label(); + usagesLabel.getStyleClass().addAll("small-font", "secondary-color-text-fill"); + Profile.getCurrent().dataSource().mapRepoAsync( + TransactionRepository.class, + repo -> repo.countTagUsages(name) + ).thenAccept(count -> Platform.runLater(() -> usagesLabel.setText("Tagged transactions: " + count))); + VBox contentBox = new VBox(nameLabel, usagesLabel); + tile.setLeft(contentBox); + Button removeButton = new Button("Remove"); + removeButton.setOnAction(event -> { + boolean confirm = Popups.confirm(removeButton, "Are you sure you want to remove this tag? It will be removed from any transactions. This cannot be undone."); + if (confirm) { + Profile.getCurrent().dataSource().useRepo( + TransactionRepository.class, + repo -> repo.deleteTag(name) + ); + refreshTags(); + } + }); + tile.setRight(removeButton); + return tile; + } +} diff --git a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java index 7865a70..e5d845c 100644 --- a/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/TransactionRepository.java @@ -36,6 +36,8 @@ public interface TransactionRepository extends Repository, AutoCloseable { List findAttachments(long transactionId); List findTags(long transactionId); List findAllTags(); + void deleteTag(String name); + long countTagUsages(String name); void delete(long transactionId); void update( long id, diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java index 4c1f720..497ad27 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcTransactionRepository.java @@ -246,6 +246,27 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem ); } + @Override + public void deleteTag(String name) { + DbUtil.update( + conn, + "DELETE FROM transaction_tag WHERE name = ?", + name + ); + } + + @Override + public long countTagUsages(String name) { + return DbUtil.count( + conn, + """ + SELECT COUNT(transaction_id) + FROM transaction_tag_join + WHERE tag_id = (SELECT id FROM transaction_tag WHERE name = ?)""", + name + ); + } + @Override public void delete(long transactionId) { DbUtil.doTransaction(conn, () -> { diff --git a/src/main/java/com/andrewlalis/perfin/view/component/CategoryTile.java b/src/main/java/com/andrewlalis/perfin/view/component/CategoryTile.java index 979f736..73f4f2f 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/CategoryTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/CategoryTile.java @@ -18,7 +18,7 @@ public class CategoryTile extends VBox { TransactionCategoryRepository.CategoryTreeNode treeNode, Runnable categoriesRefresh ) { - this.getStyleClass().addAll("tile", "hand-cursor"); + this.getStyleClass().addAll("tile", "spacing-extra", "hand-cursor"); this.setStyle("-fx-border-width: 1px; -fx-border-color: grey;"); this.setOnMouseClicked(event -> { event.consume(); diff --git a/src/main/resources/categories-view.fxml b/src/main/resources/categories-view.fxml index b92e9a7..9b0641c 100644 --- a/src/main/resources/categories-view.fxml +++ b/src/main/resources/categories-view.fxml @@ -6,6 +6,7 @@ +
+ + Categories are used to group your transactions based on their + purpose. It's helpful to categorize transactions in order to get + a better view of your spending habits, and it makes it easier to + lookup transactions later. + -