Added tags view, and cleaned up some other parts of the app.

This commit is contained in:
Andrew Lalis 2024-01-31 11:05:09 -05:00
parent aaa1081ddf
commit 39794e36a2
10 changed files with 145 additions and 4 deletions

View File

@ -95,6 +95,7 @@ public class PerfinApp extends Application {
router.map("edit-vendor", PerfinApp.class.getResource("/edit-vendor.fxml")); router.map("edit-vendor", PerfinApp.class.getResource("/edit-vendor.fxml"));
router.map("categories", PerfinApp.class.getResource("/categories-view.fxml")); router.map("categories", PerfinApp.class.getResource("/categories-view.fxml"));
router.map("edit-category", PerfinApp.class.getResource("/edit-category.fxml")); router.map("edit-category", PerfinApp.class.getResource("/edit-category.fxml"));
router.map("tags", PerfinApp.class.getResource("/tags-view.fxml"));
// Help pages. // Help pages.
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml")); helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));

View File

@ -11,6 +11,8 @@ import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import static com.andrewlalis.perfin.PerfinApp.router;
public class CategoriesViewController implements RouteSelectionListener { public class CategoriesViewController implements RouteSelectionListener {
@FXML public VBox categoriesVBox; @FXML public VBox categoriesVBox;
private final ObservableList<TransactionCategoryRepository.CategoryTreeNode> categoryTreeNodes = FXCollections.observableArrayList(); private final ObservableList<TransactionCategoryRepository.CategoryTreeNode> categoryTreeNodes = FXCollections.observableArrayList();
@ -24,6 +26,10 @@ public class CategoriesViewController implements RouteSelectionListener {
refreshCategories(); refreshCategories();
} }
@FXML public void addCategory() {
router.navigate("edit-category");
}
private void refreshCategories() { private void refreshCategories() {
Profile.getCurrent().dataSource().mapRepoAsync( Profile.getCurrent().dataSource().mapRepoAsync(
TransactionCategoryRepository.class, TransactionCategoryRepository.class,

View File

@ -92,10 +92,10 @@ public class EditTransactionController implements RouteSelectionListener {
).validatedInitially().attach(descriptionField, descriptionField.textProperty()); ).validatedInitially().attach(descriptionField, descriptionField.textProperty());
var linkedAccountsValid = initializeLinkedAccountsValidationUi(); var linkedAccountsValid = initializeLinkedAccountsValidationUi();
initializeTagSelectionUi(); initializeTagSelectionUi();
// Setup hyperlinks.
vendorsHyperlink.setOnAction(event -> router.navigate("vendors")); vendorsHyperlink.setOnAction(event -> router.navigate("vendors"));
categoriesHyperlink.setOnAction(event -> router.navigate("categories")); 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); var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
saveButton.disableProperty().bind(formValid.not()); saveButton.disableProperty().bind(formValid.not());

View File

@ -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<String> 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;
}
}

View File

@ -36,6 +36,8 @@ public interface TransactionRepository extends Repository, AutoCloseable {
List<Attachment> findAttachments(long transactionId); List<Attachment> findAttachments(long transactionId);
List<String> findTags(long transactionId); List<String> findTags(long transactionId);
List<String> findAllTags(); List<String> findAllTags();
void deleteTag(String name);
long countTagUsages(String name);
void delete(long transactionId); void delete(long transactionId);
void update( void update(
long id, long id,

View File

@ -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 @Override
public void delete(long transactionId) { public void delete(long transactionId) {
DbUtil.doTransaction(conn, () -> { DbUtil.doTransaction(conn, () -> {

View File

@ -18,7 +18,7 @@ public class CategoryTile extends VBox {
TransactionCategoryRepository.CategoryTreeNode treeNode, TransactionCategoryRepository.CategoryTreeNode treeNode,
Runnable categoriesRefresh 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.setStyle("-fx-border-width: 1px; -fx-border-color: grey;");
this.setOnMouseClicked(event -> { this.setOnMouseClicked(event -> {
event.consume(); event.consume();

View File

@ -6,6 +6,7 @@
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?> <?import javafx.scene.control.ScrollPane?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<BorderPane xmlns="http://javafx.com/javafx" <BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.CategoriesViewController" fx:controller="com.andrewlalis.perfin.control.CategoriesViewController"
@ -15,8 +16,14 @@
</top> </top>
<center> <center>
<VBox> <VBox>
<StyledText maxWidth="500" styleClass="std-padding">
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.
</StyledText>
<HBox styleClass="std-padding, std-spacing" VBox.vgrow="NEVER"> <HBox styleClass="std-padding, std-spacing" VBox.vgrow="NEVER">
<Button text="Add Category"/> <Button text="Add Category" onAction="#addCategory"/>
</HBox> </HBox>
<ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS"> <ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
<VBox fx:id="categoriesVBox" styleClass="tile-container"/> <VBox fx:id="categoriesVBox" styleClass="tile-container"/>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.TagsViewController"
>
<top>
<Label text="Transaction Tags" styleClass="large-font,bold-text,std-padding"/>
</top>
<center>
<VBox>
<StyledText maxWidth="500" styleClass="std-padding">
Transaction tags are just bits of text that can be applied to a
transaction to give it additional meaning or make searching for
certain transactions easier.
--
Tags are automatically created if you add a new one to a
transaction, and they'll show up here. When you remove a tag,
it will be permanently removed from **all** transactions that it
was previously associated with.
</StyledText>
<ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
<VBox fx:id="tagsVBox" styleClass="tile-container"/>
</ScrollPane>
</VBox>
</center>
</BorderPane>

View File

@ -4,6 +4,7 @@
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?> <?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import com.andrewlalis.perfin.view.component.StyledText?>
<BorderPane xmlns="http://javafx.com/javafx" <BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.VendorsViewController" fx:controller="com.andrewlalis.perfin.control.VendorsViewController"
@ -13,6 +14,13 @@
</top> </top>
<center> <center>
<VBox> <VBox>
<StyledText maxWidth="500" styleClass="std-padding">
Vendors are businesses or other financial entities with which
you do transactions. By tagging a vendor on your transactions,
it becomes easier to find out just how much money you're
spending at certain shops, and how often. It can also make it a
lot easier to look up past transactions.
</StyledText>
<HBox styleClass="std-padding,std-spacing" VBox.vgrow="NEVER"> <HBox styleClass="std-padding,std-spacing" VBox.vgrow="NEVER">
<Button text="Add Vendor" onAction="#addVendor"/> <Button text="Add Vendor" onAction="#addVendor"/>
</HBox> </HBox>