Add Transaction Properties #15
|
@ -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"));
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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, () -> {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue