Add Transaction Properties #15
|
@ -93,6 +93,8 @@ public class PerfinApp extends Application {
|
||||||
router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
|
router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
|
||||||
router.map("vendors", PerfinApp.class.getResource("/vendors-view.fxml"));
|
router.map("vendors", PerfinApp.class.getResource("/vendors-view.fxml"));
|
||||||
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("edit-category", PerfinApp.class.getResource("/edit-category.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"));
|
||||||
|
|
|
@ -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 ComboBox<String> vendorComboBox;
|
||||||
@FXML public Hyperlink vendorsHyperlink;
|
@FXML public Hyperlink vendorsHyperlink;
|
||||||
@FXML public ComboBox<String> categoryComboBox;
|
@FXML public ComboBox<String> categoryComboBox;
|
||||||
|
@FXML public Hyperlink categoriesHyperlink;
|
||||||
@FXML public ComboBox<String> tagsComboBox;
|
@FXML public ComboBox<String> tagsComboBox;
|
||||||
|
@FXML public Hyperlink tagsHyperlink;
|
||||||
@FXML public Button addTagButton;
|
@FXML public Button addTagButton;
|
||||||
@FXML public VBox tagsVBox;
|
@FXML public VBox tagsVBox;
|
||||||
private final ObservableList<String> selectedTags = FXCollections.observableArrayList();
|
private final ObservableList<String> selectedTags = FXCollections.observableArrayList();
|
||||||
|
@ -92,6 +94,8 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
initializeTagSelectionUi();
|
initializeTagSelectionUi();
|
||||||
// Setup hyperlinks.
|
// Setup hyperlinks.
|
||||||
vendorsHyperlink.setOnAction(event -> router.navigate("vendors"));
|
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);
|
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
||||||
saveButton.disableProperty().bind(formValid.not());
|
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.Profile;
|
||||||
import com.andrewlalis.perfin.model.TransactionVendor;
|
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||||
import com.andrewlalis.perfin.view.BindingUtil;
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
|
import com.andrewlalis.perfin.view.component.VendorTile;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
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 javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -25,36 +21,7 @@ public class VendorsViewController implements RouteSelectionListener {
|
||||||
private final ObservableList<TransactionVendor> vendors = FXCollections.observableArrayList();
|
private final ObservableList<TransactionVendor> vendors = FXCollections.observableArrayList();
|
||||||
|
|
||||||
@FXML public void initialize() {
|
@FXML public void initialize() {
|
||||||
BindingUtil.mapContent(vendorsVBox.getChildren(), vendors, this::buildVendorTile);
|
BindingUtil.mapContent(vendorsVBox.getChildren(), vendors, vendor -> new VendorTile(vendor, this::refreshVendors));
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -13,5 +13,9 @@ public interface TransactionCategoryRepository extends Repository, AutoCloseable
|
||||||
List<TransactionCategory> findAll();
|
List<TransactionCategory> findAll();
|
||||||
long insert(long parentId, String name, Color color);
|
long insert(long parentId, String name, Color color);
|
||||||
long insert(String name, Color color);
|
long insert(String name, Color color);
|
||||||
|
void update(long id, String name, Color color);
|
||||||
void deleteById(long id);
|
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.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
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
|
@Override
|
||||||
public void deleteById(long id) {
|
public void deleteById(long id) {
|
||||||
DbUtil.updateOne(conn, "DELETE FROM transaction_category WHERE id = ?", 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
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
conn.close();
|
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>
|
<VBox>
|
||||||
<Label text="Category" labelFor="${categoryComboBox}" styleClass="bold-text"/>
|
<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>
|
</VBox>
|
||||||
<ComboBox fx:id="categoryComboBox" editable="true" maxWidth="Infinity"/>
|
<ComboBox fx:id="categoryComboBox" editable="true" maxWidth="Infinity"/>
|
||||||
|
|
||||||
<VBox>
|
<VBox>
|
||||||
<Label text="Tags" labelFor="${tagsComboBox}" styleClass="bold-text"/>
|
<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>
|
||||||
<VBox maxWidth="Infinity">
|
<VBox maxWidth="Infinity">
|
||||||
<HBox styleClass="std-spacing">
|
<HBox styleClass="std-spacing">
|
||||||
|
|
|
@ -3,10 +3,7 @@
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.ScrollPane?>
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
<?import javafx.scene.layout.BorderPane?>
|
<?import javafx.scene.layout.*?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
|
||||||
<?import javafx.scene.layout.VBox?>
|
|
||||||
<?import javafx.scene.text.Text?>
|
|
||||||
<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"
|
||||||
|
|
Loading…
Reference in New Issue