Add Transaction Properties #15
|
@ -10,13 +10,16 @@ import com.andrewlalis.perfin.data.util.DateUtil;
|
||||||
import com.andrewlalis.perfin.model.*;
|
import com.andrewlalis.perfin.model.*;
|
||||||
import com.andrewlalis.perfin.view.BindingUtil;
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
|
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
|
||||||
|
import com.andrewlalis.perfin.view.component.CategorySelectionBox;
|
||||||
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||||
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.BooleanExpression;
|
import javafx.beans.binding.BooleanExpression;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
@ -61,7 +64,7 @@ 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 CategorySelectionBox categoryComboBox;
|
||||||
@FXML public Hyperlink categoriesHyperlink;
|
@FXML public Hyperlink categoriesHyperlink;
|
||||||
@FXML public ComboBox<String> tagsComboBox;
|
@FXML public ComboBox<String> tagsComboBox;
|
||||||
@FXML public Hyperlink tagsHyperlink;
|
@FXML public Hyperlink tagsHyperlink;
|
||||||
|
@ -69,6 +72,15 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
@FXML public VBox tagsVBox;
|
@FXML public VBox tagsVBox;
|
||||||
private final ObservableList<String> selectedTags = FXCollections.observableArrayList();
|
private final ObservableList<String> selectedTags = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
@FXML public Spinner<Integer> lineItemQuantitySpinner;
|
||||||
|
@FXML public TextField lineItemValueField;
|
||||||
|
@FXML public TextField lineItemDescriptionField;
|
||||||
|
@FXML public Button addLineItemButton;
|
||||||
|
@FXML public VBox addLineItemForm;
|
||||||
|
@FXML public Button addLineItemAddButton;
|
||||||
|
@FXML public Button addLineItemCancelButton;
|
||||||
|
@FXML public final BooleanProperty addingLineItemProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
@FXML public FileSelectionArea attachmentsSelectionArea;
|
@FXML public FileSelectionArea attachmentsSelectionArea;
|
||||||
|
|
||||||
@FXML public Button saveButton;
|
@FXML public Button saveButton;
|
||||||
|
@ -97,6 +109,26 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
categoriesHyperlink.setOnAction(event -> router.navigate("categories"));
|
categoriesHyperlink.setOnAction(event -> router.navigate("categories"));
|
||||||
tagsHyperlink.setOnAction(event -> router.navigate("tags"));
|
tagsHyperlink.setOnAction(event -> router.navigate("tags"));
|
||||||
|
|
||||||
|
// Initialize line item stuff.
|
||||||
|
addLineItemButton.setOnAction(event -> addingLineItemProperty.set(true));
|
||||||
|
addLineItemCancelButton.setOnAction(event -> {
|
||||||
|
lineItemQuantitySpinner.getValueFactory().setValue(1);
|
||||||
|
lineItemValueField.setText(null);
|
||||||
|
lineItemDescriptionField.setText(null);
|
||||||
|
addingLineItemProperty.set(false);
|
||||||
|
});
|
||||||
|
BindingUtil.bindManagedAndVisible(addLineItemButton, addingLineItemProperty.not());
|
||||||
|
BindingUtil.bindManagedAndVisible(addLineItemForm, addingLineItemProperty);
|
||||||
|
lineItemQuantitySpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, Integer.MAX_VALUE, 1, 1));
|
||||||
|
var lineItemValueValid = new ValidationApplier<>(
|
||||||
|
new CurrencyAmountValidator(() -> currencyChoiceBox.getValue(), false, false)
|
||||||
|
).attachToTextField(lineItemValueField, currencyChoiceBox.valueProperty());
|
||||||
|
var lineItemDescriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
|
.addTerminalPredicate(s -> s != null && !s.isBlank(), "A description is required.")
|
||||||
|
).attachToTextField(lineItemDescriptionField);
|
||||||
|
var lineItemFormValid = lineItemValueValid.and(lineItemDescriptionValid);
|
||||||
|
addLineItemAddButton.disableProperty().bind(lineItemFormValid.not());
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
@ -108,7 +140,7 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
String description = getSanitizedDescription();
|
String description = getSanitizedDescription();
|
||||||
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
|
CreditAndDebitAccounts linkedAccounts = getSelectedAccounts();
|
||||||
String vendor = vendorComboBox.getValue();
|
String vendor = vendorComboBox.getValue();
|
||||||
String category = categoryComboBox.getValue();
|
String category = categoryComboBox.getValue() == null ? null : categoryComboBox.getValue().getName();
|
||||||
Set<String> tags = new HashSet<>(selectedTags);
|
Set<String> tags = new HashSet<>(selectedTags);
|
||||||
List<Path> newAttachmentPaths = attachmentsSelectionArea.getSelectedPaths();
|
List<Path> newAttachmentPaths = attachmentsSelectionArea.getSelectedPaths();
|
||||||
List<Attachment> existingAttachments = attachmentsSelectionArea.getSelectedAttachments();
|
List<Attachment> existingAttachments = attachmentsSelectionArea.getSelectedAttachments();
|
||||||
|
@ -161,7 +193,7 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
// Clear some initial fields immediately:
|
// Clear some initial fields immediately:
|
||||||
tagsComboBox.setValue(null);
|
tagsComboBox.setValue(null);
|
||||||
vendorComboBox.setValue(null);
|
vendorComboBox.setValue(null);
|
||||||
categoryComboBox.setValue(null);
|
categoryComboBox.select(null);
|
||||||
|
|
||||||
if (transaction == null) {
|
if (transaction == null) {
|
||||||
titleLabel.setText("Create New Transaction");
|
titleLabel.setText("Create New Transaction");
|
||||||
|
@ -191,17 +223,18 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
.toList();
|
.toList();
|
||||||
List<Account> accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
|
List<Account> accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
|
||||||
final List<Attachment> attachments;
|
final List<Attachment> attachments;
|
||||||
|
final var categoryTreeNodes = categoryRepo.findTree();
|
||||||
final List<String> availableTags = transactionRepo.findAllTags();
|
final List<String> availableTags = transactionRepo.findAllTags();
|
||||||
final List<String> tags;
|
final List<String> tags;
|
||||||
final CreditAndDebitAccounts linkedAccounts;
|
final CreditAndDebitAccounts linkedAccounts;
|
||||||
final String vendorName;
|
final String vendorName;
|
||||||
final String categoryName;
|
final TransactionCategory category;
|
||||||
if (transaction == null) {
|
if (transaction == null) {
|
||||||
attachments = Collections.emptyList();
|
attachments = Collections.emptyList();
|
||||||
tags = Collections.emptyList();
|
tags = Collections.emptyList();
|
||||||
linkedAccounts = new CreditAndDebitAccounts(null, null);
|
linkedAccounts = new CreditAndDebitAccounts(null, null);
|
||||||
vendorName = null;
|
vendorName = null;
|
||||||
categoryName = null;
|
category = null;
|
||||||
} else {
|
} else {
|
||||||
attachments = transactionRepo.findAttachments(transaction.id);
|
attachments = transactionRepo.findAttachments(transaction.id);
|
||||||
tags = transactionRepo.findTags(transaction.id);
|
tags = transactionRepo.findTags(transaction.id);
|
||||||
|
@ -213,14 +246,12 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
vendorName = null;
|
vendorName = null;
|
||||||
}
|
}
|
||||||
if (transaction.getCategoryId() != null) {
|
if (transaction.getCategoryId() != null) {
|
||||||
categoryName = categoryRepo.findById(transaction.getCategoryId())
|
category = categoryRepo.findById(transaction.getCategoryId()).orElse(null);
|
||||||
.map(TransactionCategory::getName).orElse(null);
|
|
||||||
} else {
|
} else {
|
||||||
categoryName = null;
|
category = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final List<TransactionVendor> availableVendors = vendorRepo.findAll();
|
final List<TransactionVendor> availableVendors = vendorRepo.findAll();
|
||||||
final List<TransactionCategory> availableCategories = categoryRepo.findAll();
|
|
||||||
// Then make updates to the view.
|
// Then make updates to the view.
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
currencyChoiceBox.getItems().setAll(currencies);
|
currencyChoiceBox.getItems().setAll(currencies);
|
||||||
|
@ -228,12 +259,13 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
debitAccountSelector.setAccounts(accounts);
|
debitAccountSelector.setAccounts(accounts);
|
||||||
vendorComboBox.getItems().setAll(availableVendors.stream().map(TransactionVendor::getName).toList());
|
vendorComboBox.getItems().setAll(availableVendors.stream().map(TransactionVendor::getName).toList());
|
||||||
vendorComboBox.setValue(vendorName);
|
vendorComboBox.setValue(vendorName);
|
||||||
categoryComboBox.getItems().setAll(availableCategories.stream().map(TransactionCategory::getName).toList());
|
categoryComboBox.loadCategories(categoryTreeNodes);
|
||||||
categoryComboBox.setValue(categoryName);
|
categoryComboBox.select(category);
|
||||||
tagsComboBox.getItems().setAll(availableTags);
|
tagsComboBox.getItems().setAll(availableTags);
|
||||||
attachmentsSelectionArea.clear();
|
attachmentsSelectionArea.clear();
|
||||||
attachmentsSelectionArea.addAttachments(attachments);
|
attachmentsSelectionArea.addAttachments(attachments);
|
||||||
selectedTags.clear();
|
selectedTags.clear();
|
||||||
|
selectedTags.addAll(tags);
|
||||||
if (transaction == null) {
|
if (transaction == null) {
|
||||||
currencyChoiceBox.getSelectionModel().selectFirst();
|
currencyChoiceBox.getSelectionModel().selectFirst();
|
||||||
creditAccountSelector.select(null);
|
creditAccountSelector.select(null);
|
||||||
|
@ -242,7 +274,6 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
|
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
|
||||||
creditAccountSelector.select(linkedAccounts.creditAccount());
|
creditAccountSelector.select(linkedAccounts.creditAccount());
|
||||||
debitAccountSelector.select(linkedAccounts.debitAccount());
|
debitAccountSelector.select(linkedAccounts.debitAccount());
|
||||||
selectedTags.addAll(tags);
|
|
||||||
}
|
}
|
||||||
container.setDisable(false);
|
container.setDisable(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.andrewlalis.perfin.control;
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
|
@ -54,6 +55,10 @@ public class Popups {
|
||||||
error(getWindowFromNode(node), e);
|
error(getWindowFromNode(node), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void errorLater(Node node, Exception e) {
|
||||||
|
Platform.runLater(() -> error(node, e));
|
||||||
|
}
|
||||||
|
|
||||||
private static Window getWindowFromNode(Node n) {
|
private static Window getWindowFromNode(Node n) {
|
||||||
Window owner = null;
|
Window owner = null;
|
||||||
Scene scene = n.getScene();
|
Scene scene = n.getScene();
|
||||||
|
|
|
@ -3,23 +3,37 @@ package com.andrewlalis.perfin.control;
|
||||||
import com.andrewlalis.perfin.data.TransactionRepository;
|
import com.andrewlalis.perfin.data.TransactionRepository;
|
||||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||||
import com.andrewlalis.perfin.model.Attachment;
|
import com.andrewlalis.perfin.model.*;
|
||||||
import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
|
||||||
import com.andrewlalis.perfin.model.Transaction;
|
|
||||||
import com.andrewlalis.perfin.view.component.AttachmentsViewPane;
|
import com.andrewlalis.perfin.view.component.AttachmentsViewPane;
|
||||||
|
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.ListProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleListProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Hyperlink;
|
import javafx.scene.control.Hyperlink;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
|
import org.slf4j.Logger;
|
||||||
import java.util.List;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
public class TransactionViewController {
|
public class TransactionViewController {
|
||||||
private Transaction transaction;
|
private static final Logger log = LoggerFactory.getLogger(TransactionViewController.class);
|
||||||
|
|
||||||
|
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
|
||||||
|
private final ObjectProperty<CreditAndDebitAccounts> linkedAccountsProperty = new SimpleObjectProperty<>(null);
|
||||||
|
private final ObjectProperty<TransactionVendor> vendorProperty = new SimpleObjectProperty<>(null);
|
||||||
|
private final ObjectProperty<TransactionCategory> categoryProperty = new SimpleObjectProperty<>(null);
|
||||||
|
private final ObservableList<String> tagsList = FXCollections.observableArrayList();
|
||||||
|
private final ListProperty<String> tagsListProperty = new SimpleListProperty<>(tagsList);
|
||||||
|
private final ObservableList<Attachment> attachmentsList = FXCollections.observableArrayList();
|
||||||
|
|
||||||
@FXML public Label titleLabel;
|
@FXML public Label titleLabel;
|
||||||
|
|
||||||
|
@ -27,47 +41,103 @@ public class TransactionViewController {
|
||||||
@FXML public Label timestampLabel;
|
@FXML public Label timestampLabel;
|
||||||
@FXML public Label descriptionLabel;
|
@FXML public Label descriptionLabel;
|
||||||
|
|
||||||
|
@FXML public Label vendorLabel;
|
||||||
|
@FXML public Circle categoryColorIndicator;
|
||||||
|
@FXML public Label categoryLabel;
|
||||||
|
@FXML public Label tagsLabel;
|
||||||
|
|
||||||
@FXML public Hyperlink debitAccountLink;
|
@FXML public Hyperlink debitAccountLink;
|
||||||
@FXML public Hyperlink creditAccountLink;
|
@FXML public Hyperlink creditAccountLink;
|
||||||
|
|
||||||
@FXML public AttachmentsViewPane attachmentsViewPane;
|
@FXML public AttachmentsViewPane attachmentsViewPane;
|
||||||
|
|
||||||
@FXML public void initialize() {
|
@FXML public void initialize() {
|
||||||
configureAccountLinkBindings(debitAccountLink);
|
titleLabel.textProperty().bind(transactionProperty.map(t -> "Transaction #" + t.id));
|
||||||
configureAccountLinkBindings(creditAccountLink);
|
amountLabel.textProperty().bind(transactionProperty.map(t -> CurrencyUtil.formatMoney(t.getMoneyAmount())));
|
||||||
|
timestampLabel.textProperty().bind(transactionProperty.map(t -> DateUtil.formatUTCAsLocalWithZone(t.getTimestamp())));
|
||||||
|
descriptionLabel.textProperty().bind(transactionProperty.map(Transaction::getDescription));
|
||||||
|
|
||||||
|
PropertiesPane vendorPane = (PropertiesPane) vendorLabel.getParent();
|
||||||
|
BindingUtil.bindManagedAndVisible(vendorPane, vendorProperty.isNotNull());
|
||||||
|
vendorLabel.textProperty().bind(vendorProperty.map(TransactionVendor::getName));
|
||||||
|
|
||||||
|
PropertiesPane categoryPane = (PropertiesPane) categoryLabel.getParent().getParent();
|
||||||
|
BindingUtil.bindManagedAndVisible(categoryPane, categoryProperty.isNotNull());
|
||||||
|
categoryLabel.textProperty().bind(categoryProperty.map(TransactionCategory::getName));
|
||||||
|
categoryColorIndicator.fillProperty().bind(categoryProperty.map(TransactionCategory::getColor));
|
||||||
|
|
||||||
|
PropertiesPane tagsPane = (PropertiesPane) tagsLabel.getParent();
|
||||||
|
BindingUtil.bindManagedAndVisible(tagsPane, tagsListProperty.emptyProperty().not());
|
||||||
|
tagsLabel.textProperty().bind(tagsListProperty.map(tags -> String.join(", ", tags)));
|
||||||
|
|
||||||
|
TextFlow debitText = (TextFlow) debitAccountLink.getParent();
|
||||||
|
BindingUtil.bindManagedAndVisible(debitText, linkedAccountsProperty.map(CreditAndDebitAccounts::hasDebit));
|
||||||
|
debitAccountLink.textProperty().bind(linkedAccountsProperty.map(la -> la.hasDebit() ? la.debitAccount().getShortName() : null));
|
||||||
|
debitAccountLink.onActionProperty().bind(linkedAccountsProperty.map(la -> {
|
||||||
|
if (la.hasDebit()) {
|
||||||
|
return event -> router.navigate("account", la.debitAccount());
|
||||||
|
}
|
||||||
|
return event -> {};
|
||||||
|
}));
|
||||||
|
TextFlow creditText = (TextFlow) creditAccountLink.getParent();
|
||||||
|
BindingUtil.bindManagedAndVisible(creditText, linkedAccountsProperty.map(CreditAndDebitAccounts::hasCredit));
|
||||||
|
creditAccountLink.textProperty().bind(linkedAccountsProperty.map(la -> la.hasCredit() ? la.creditAccount().getShortName() : null));
|
||||||
|
creditAccountLink.onActionProperty().bind(linkedAccountsProperty.map(la -> {
|
||||||
|
if (la.hasCredit()) {
|
||||||
|
return event -> router.navigate("account", la.creditAccount());
|
||||||
|
}
|
||||||
|
return event -> {};
|
||||||
|
}));
|
||||||
|
|
||||||
attachmentsViewPane.hideIfEmpty();
|
attachmentsViewPane.hideIfEmpty();
|
||||||
|
attachmentsViewPane.listProperty().bindContent(attachmentsList);
|
||||||
|
|
||||||
|
transactionProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (newValue == null) {
|
||||||
|
linkedAccountsProperty.set(null);
|
||||||
|
vendorProperty.set(null);
|
||||||
|
categoryProperty.set(null);
|
||||||
|
tagsList.clear();
|
||||||
|
attachmentsList.clear();
|
||||||
|
} else {
|
||||||
|
updateLinkedData(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTransaction(Transaction transaction) {
|
public void setTransaction(Transaction transaction) {
|
||||||
this.transaction = transaction;
|
this.transactionProperty.set(transaction);
|
||||||
if (transaction == null) return;
|
}
|
||||||
titleLabel.setText("Transaction #" + transaction.id);
|
|
||||||
amountLabel.setText(CurrencyUtil.formatMoney(transaction.getMoneyAmount()));
|
private void updateLinkedData(Transaction tx) {
|
||||||
timestampLabel.setText(DateUtil.formatUTCAsLocalWithZone(transaction.getTimestamp()));
|
var ds = Profile.getCurrent().dataSource();
|
||||||
descriptionLabel.setText(transaction.getDescription());
|
Thread.ofVirtual().start(() -> {
|
||||||
Profile.getCurrent().dataSource().useRepoAsync(TransactionRepository.class, repo -> {
|
try (
|
||||||
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
|
var transactionRepo = ds.getTransactionRepository();
|
||||||
List<Attachment> attachments = repo.findAttachments(transaction.id);
|
var vendorRepo = ds.getTransactionVendorRepository();
|
||||||
|
var categoryRepo = ds.getTransactionCategoryRepository()
|
||||||
|
) {
|
||||||
|
final var linkedAccounts = transactionRepo.findLinkedAccounts(tx.id);
|
||||||
|
final var vendor = tx.getVendorId() == null ? null : vendorRepo.findById(tx.getVendorId()).orElse(null);
|
||||||
|
final var category = tx.getCategoryId() == null ? null : categoryRepo.findById(tx.getCategoryId()).orElse(null);
|
||||||
|
final var attachments = transactionRepo.findAttachments(tx.id);
|
||||||
|
final var tags = transactionRepo.findTags(tx.id);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (accounts.hasDebit()) {
|
linkedAccountsProperty.set(linkedAccounts);
|
||||||
debitAccountLink.setText(accounts.debitAccount().getShortName());
|
vendorProperty.set(vendor);
|
||||||
debitAccountLink.setOnAction(event -> router.navigate("account", accounts.debitAccount()));
|
categoryProperty.set(category);
|
||||||
} else {
|
attachmentsList.setAll(attachments);
|
||||||
debitAccountLink.setText(null);
|
tagsList.setAll(tags);
|
||||||
}
|
|
||||||
if (accounts.hasCredit()) {
|
|
||||||
creditAccountLink.setText(accounts.creditAccount().getShortName());
|
|
||||||
creditAccountLink.setOnAction(event -> router.navigate("account", accounts.creditAccount()));
|
|
||||||
} else {
|
|
||||||
creditAccountLink.setText(null);
|
|
||||||
}
|
|
||||||
attachmentsViewPane.setAttachments(attachments);
|
|
||||||
});
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to fetch additional transaction data.", e);
|
||||||
|
Popups.errorLater(titleLabel, e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void editTransaction() {
|
@FXML public void editTransaction() {
|
||||||
router.navigate("edit-transaction", this.transaction);
|
router.navigate("edit-transaction", this.transactionProperty.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void deleteTransaction() {
|
@FXML public void deleteTransaction() {
|
||||||
|
@ -82,15 +152,8 @@ public class TransactionViewController {
|
||||||
"it's derived from the most recent balance-record, and transactions."
|
"it's derived from the most recent balance-record, and transactions."
|
||||||
);
|
);
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
Profile.getCurrent().dataSource().useRepo(TransactionRepository.class, repo -> repo.delete(transaction.id));
|
Profile.getCurrent().dataSource().useRepo(TransactionRepository.class, repo -> repo.delete(this.transactionProperty.get().id));
|
||||||
router.replace("transactions");
|
router.replace("transactions");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureAccountLinkBindings(Hyperlink link) {
|
|
||||||
TextFlow parent = (TextFlow) link.getParent();
|
|
||||||
parent.managedProperty().bind(parent.visibleProperty());
|
|
||||||
parent.visibleProperty().bind(link.textProperty().isNotEmpty());
|
|
||||||
link.setText(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.andrewlalis.perfin.data.util.Pair;
|
||||||
import com.andrewlalis.perfin.model.Account;
|
import com.andrewlalis.perfin.model.Account;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.model.Transaction;
|
import com.andrewlalis.perfin.model.Transaction;
|
||||||
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
import com.andrewlalis.perfin.view.SceneUtil;
|
import com.andrewlalis.perfin.view.SceneUtil;
|
||||||
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
|
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
|
||||||
import com.andrewlalis.perfin.view.component.DataSourcePaginationControls;
|
import com.andrewlalis.perfin.view.component.DataSourcePaginationControls;
|
||||||
|
@ -98,14 +99,11 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
detailPanel.minWidthProperty().bind(halfWidthProp);
|
detailPanel.minWidthProperty().bind(halfWidthProp);
|
||||||
detailPanel.maxWidthProperty().bind(halfWidthProp);
|
detailPanel.maxWidthProperty().bind(halfWidthProp);
|
||||||
detailPanel.prefWidthProperty().bind(halfWidthProp);
|
detailPanel.prefWidthProperty().bind(halfWidthProp);
|
||||||
detailPanel.managedProperty().bind(detailPanel.visibleProperty());
|
BindingUtil.bindManagedAndVisible(detailPanel, selectedTransaction.isNotNull());
|
||||||
detailPanel.visibleProperty().bind(selectedTransaction.isNotNull());
|
|
||||||
|
|
||||||
Pair<BorderPane, TransactionViewController> detailComponents = SceneUtil.loadNodeAndController("/transaction-view.fxml");
|
Pair<BorderPane, TransactionViewController> detailComponents = SceneUtil.loadNodeAndController("/transaction-view.fxml");
|
||||||
TransactionViewController transactionViewController = detailComponents.second();
|
TransactionViewController transactionViewController = detailComponents.second();
|
||||||
BorderPane transactionDetailView = detailComponents.first();
|
BorderPane transactionDetailView = detailComponents.first();
|
||||||
transactionDetailView.managedProperty().bind(transactionDetailView.visibleProperty());
|
|
||||||
transactionDetailView.visibleProperty().bind(selectedTransaction.isNotNull());
|
|
||||||
detailPanel.getChildren().add(transactionDetailView);
|
detailPanel.getChildren().add(transactionDetailView);
|
||||||
selectedTransaction.addListener((observable, oldValue, newValue) -> {
|
selectedTransaction.addListener((observable, oldValue, newValue) -> {
|
||||||
transactionViewController.setTransaction(newValue);
|
transactionViewController.setTransaction(newValue);
|
||||||
|
@ -121,7 +119,7 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
@Override
|
@Override
|
||||||
public void onRouteSelected(Object context) {
|
public void onRouteSelected(Object context) {
|
||||||
paginationControls.sorts.setAll(DEFAULT_SORTS);
|
paginationControls.sorts.setAll(DEFAULT_SORTS);
|
||||||
transactionsVBox.getChildren().clear(); // Clear the transactions before reload initially.
|
selectedTransaction.set(null); // Initially set the selected transaction as null.
|
||||||
|
|
||||||
// Refresh account filter options.
|
// Refresh account filter options.
|
||||||
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
|
Profile.getCurrent().dataSource().useRepoAsync(AccountRepository.class, repo -> {
|
||||||
|
@ -140,13 +138,13 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
long offset = repo.countAllAfter(tx.id);
|
long offset = repo.countAllAfter(tx.id);
|
||||||
int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1;
|
int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1;
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
paginationControls.setPage(pageNumber).thenRun(() -> selectedTransaction.set(tx));
|
paginationControls.setPage(pageNumber);
|
||||||
|
selectedTransaction.set(tx);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
paginationControls.setPage(1);
|
paginationControls.setPage(1);
|
||||||
selectedTransaction.set(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,12 @@ import com.andrewlalis.perfin.data.DataSourceFactory;
|
||||||
import com.andrewlalis.perfin.data.ProfileLoadException;
|
import com.andrewlalis.perfin.data.ProfileLoadException;
|
||||||
import com.andrewlalis.perfin.data.impl.migration.Migration;
|
import com.andrewlalis.perfin.data.impl.migration.Migration;
|
||||||
import com.andrewlalis.perfin.data.impl.migration.Migrations;
|
import com.andrewlalis.perfin.data.impl.migration.Migrations;
|
||||||
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
import com.andrewlalis.perfin.data.util.FileUtil;
|
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -15,9 +19,7 @@ import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.sql.Connection;
|
import java.sql.*;
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.Statement;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -77,6 +79,7 @@ public class JdbcDataSourceFactory implements DataSourceFactory {
|
||||||
if (in == null) throw new IOException("Could not load database schema SQL file.");
|
if (in == null) throw new IOException("Could not load database schema SQL file.");
|
||||||
String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
executeSqlScript(schemaStr, conn);
|
executeSqlScript(schemaStr, conn);
|
||||||
|
insertDefaultData(conn);
|
||||||
try {
|
try {
|
||||||
writeCurrentSchemaVersion(profileName);
|
writeCurrentSchemaVersion(profileName);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -97,6 +100,42 @@ public class JdbcDataSourceFactory implements DataSourceFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void insertDefaultData(Connection conn) throws IOException, SQLException {
|
||||||
|
try (
|
||||||
|
var categoriesIn = JdbcDataSourceFactory.class.getResourceAsStream("/sql/data/default-categories.json");
|
||||||
|
var stmt = conn.prepareStatement(
|
||||||
|
"INSERT INTO transaction_category (parent_id, name, color) VALUES (?, ?, ?)",
|
||||||
|
Statement.RETURN_GENERATED_KEYS
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (categoriesIn == null) throw new IOException("Couldn't load default categories file.");
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
ArrayNode categoriesArray = mapper.readValue(categoriesIn, ArrayNode.class);
|
||||||
|
insertCategoriesRecursive(stmt, categoriesArray, null, "#FFFFFF");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertCategoriesRecursive(PreparedStatement stmt, ArrayNode categoriesArray, Long parentId, String parentColorHex) throws SQLException {
|
||||||
|
for (JsonNode obj : categoriesArray) {
|
||||||
|
String name = obj.get("name").asText();
|
||||||
|
String colorHex = parentColorHex;
|
||||||
|
if (obj.hasNonNull("color")) colorHex = obj.get("color").asText(parentColorHex);
|
||||||
|
if (parentId == null) {
|
||||||
|
stmt.setNull(1, Types.BIGINT);
|
||||||
|
} else {
|
||||||
|
stmt.setLong(1, parentId);
|
||||||
|
}
|
||||||
|
stmt.setString(2, name);
|
||||||
|
stmt.setString(3, colorHex.substring(1));
|
||||||
|
int result = stmt.executeUpdate();
|
||||||
|
if (result != 1) throw new SQLException("Failed to insert category.");
|
||||||
|
long id = DbUtil.getGeneratedId(stmt);
|
||||||
|
if (obj.hasNonNull("children") && obj.get("children").isArray()) {
|
||||||
|
insertCategoriesRecursive(stmt, obj.withArray("children"), id, colorHex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean testConnection(JdbcDataSource dataSource) {
|
private boolean testConnection(JdbcDataSource dataSource) {
|
||||||
try (var conn = dataSource.getConnection(); var stmt = conn.createStatement()) {
|
try (var conn = dataSource.getConnection(); var stmt = conn.createStatement()) {
|
||||||
return stmt.execute("SELECT 1;");
|
return stmt.execute("SELECT 1;");
|
||||||
|
|
|
@ -31,6 +31,15 @@ public final class DbUtil {
|
||||||
setArgs(stmt, List.of(args));
|
setArgs(stmt, List.of(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long getGeneratedId(PreparedStatement stmt) {
|
||||||
|
try (ResultSet rs = stmt.getGeneratedKeys()) {
|
||||||
|
if (!rs.next()) throw new SQLException("No generated keys available.");
|
||||||
|
return rs.getLong(1);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new UncheckedSqlException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> List<T> findAll(Connection conn, String query, List<Object> args, ResultSetMapper<T> mapper) {
|
public static <T> List<T> findAll(Connection conn, String query, List<Object> args, ResultSetMapper<T> mapper) {
|
||||||
try (var stmt = conn.prepareStatement(query)) {
|
try (var stmt = conn.prepareStatement(query)) {
|
||||||
setArgs(stmt, args);
|
setArgs(stmt, args);
|
||||||
|
@ -116,9 +125,7 @@ public final class DbUtil {
|
||||||
setArgs(stmt, args);
|
setArgs(stmt, args);
|
||||||
int result = stmt.executeUpdate();
|
int result = stmt.executeUpdate();
|
||||||
if (result != 1) throw new UncheckedSqlException("Insert query did not update 1 row.");
|
if (result != 1) throw new UncheckedSqlException("Insert query did not update 1 row.");
|
||||||
var rs = stmt.getGeneratedKeys();
|
return getGeneratedId(stmt);
|
||||||
if (!rs.next()) throw new UncheckedSqlException("Insert query did not generate any keys.");
|
|
||||||
return rs.getLong(1);
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new UncheckedSqlException(e);
|
throw new UncheckedSqlException(e);
|
||||||
}
|
}
|
||||||
|
@ -155,7 +162,9 @@ public final class DbUtil {
|
||||||
public static <T> T doTransaction(Connection conn, SQLSupplier<T> supplier) {
|
public static <T> T doTransaction(Connection conn, SQLSupplier<T> supplier) {
|
||||||
try {
|
try {
|
||||||
conn.setAutoCommit(false);
|
conn.setAutoCommit(false);
|
||||||
return supplier.offer();
|
T result = supplier.offer();
|
||||||
|
conn.commit();
|
||||||
|
return result;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
try {
|
try {
|
||||||
conn.rollback();
|
conn.rollback();
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package com.andrewlalis.perfin.view;
|
package com.andrewlalis.perfin.view;
|
||||||
|
|
||||||
import javafx.beans.WeakListener;
|
import javafx.beans.WeakListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -86,4 +88,9 @@ public class BindingUtil {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void bindManagedAndVisible(Node node, ObservableValue<? extends Boolean> value) {
|
||||||
|
node.managedProperty().bind(node.visibleProperty());
|
||||||
|
node.visibleProperty().bind(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,19 +72,21 @@ public class AccountSelectionBox extends ComboBox<Account> {
|
||||||
showBalanceProperty.set(value);
|
showBalanceProperty.set(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class CellFactory implements Callback<ListView<Account>, ListCell<Account>> {
|
/**
|
||||||
private final BooleanProperty showBalanceProp;
|
* A simple cell factory that just returns instances of {@link AccountListCell}.
|
||||||
|
* @param showBalanceProp Whether to show the account's balance.
|
||||||
private CellFactory(BooleanProperty showBalanceProp) {
|
*/
|
||||||
this.showBalanceProp = showBalanceProp;
|
private record CellFactory(BooleanProperty showBalanceProp) implements Callback<ListView<Account>, ListCell<Account>> {
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListCell<Account> call(ListView<Account> param) {
|
public ListCell<Account> call(ListView<Account> param) {
|
||||||
return new AccountListCell(showBalanceProp);
|
return new AccountListCell(showBalanceProp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list cell implementation which shows an account's name, and optionally,
|
||||||
|
* its current derived balance underneath.
|
||||||
|
*/
|
||||||
private static class AccountListCell extends ListCell<Account> {
|
private static class AccountListCell extends ListCell<Account> {
|
||||||
private final BooleanProperty showBalanceProp;
|
private final BooleanProperty showBalanceProp;
|
||||||
private final Label nameLabel = new Label();
|
private final Label nameLabel = new Label();
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.andrewlalis.perfin.view.component;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.TransactionCategoryRepository;
|
||||||
|
import com.andrewlalis.perfin.model.TransactionCategory;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.shape.Circle;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class CategorySelectionBox extends ComboBox<TransactionCategory> {
|
||||||
|
private final Map<TransactionCategory, Integer> categoryIndentationLevels = new HashMap<>();
|
||||||
|
|
||||||
|
public CategorySelectionBox() {
|
||||||
|
setCellFactory(view -> new CategoryListCell(categoryIndentationLevels));
|
||||||
|
setButtonCell(new CategoryListCell(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadCategories(List<TransactionCategoryRepository.CategoryTreeNode> treeNodes) {
|
||||||
|
categoryIndentationLevels.clear();
|
||||||
|
getItems().clear();
|
||||||
|
populateCategories(treeNodes, 0);
|
||||||
|
getItems().add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateCategories(
|
||||||
|
List<TransactionCategoryRepository.CategoryTreeNode> treeNodes,
|
||||||
|
int depth
|
||||||
|
) {
|
||||||
|
for (var node : treeNodes) {
|
||||||
|
getItems().add(node.category());
|
||||||
|
categoryIndentationLevels.put(node.category(), depth);
|
||||||
|
populateCategories(node.children(), depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void select(TransactionCategory category) {
|
||||||
|
setButtonCell(new CategoryListCell(null));
|
||||||
|
getSelectionModel().select(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CategoryListCell extends ListCell<TransactionCategory> {
|
||||||
|
private final Label nameLabel = new Label();
|
||||||
|
private final Circle colorIndicator = new Circle(8);
|
||||||
|
private final Map<TransactionCategory, Integer> categoryIndentationLevels;
|
||||||
|
|
||||||
|
public CategoryListCell(Map<TransactionCategory, Integer> categoryIndentationLevels) {
|
||||||
|
this.categoryIndentationLevels = categoryIndentationLevels;
|
||||||
|
nameLabel.getStyleClass().add("normal-color-text-fill");
|
||||||
|
colorIndicator.managedProperty().bind(colorIndicator.visibleProperty());
|
||||||
|
HBox container = new HBox(colorIndicator, nameLabel);
|
||||||
|
container.getStyleClass().add("std-spacing");
|
||||||
|
setGraphic(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(TransactionCategory item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (item == null || empty) {
|
||||||
|
nameLabel.setText("None");
|
||||||
|
colorIndicator.setVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nameLabel.setText(item.getName());
|
||||||
|
if (categoryIndentationLevels != null) {
|
||||||
|
HBox.setMargin(
|
||||||
|
colorIndicator,
|
||||||
|
new Insets(0, 0, 0, 10 * categoryIndentationLevels.getOrDefault(item, 0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
colorIndicator.setVisible(true);
|
||||||
|
colorIndicator.setFill(item.getColor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,7 +55,7 @@
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<HBox styleClass="std-padding,std-spacing">
|
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_RIGHT">
|
||||||
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
<Button text="Cancel" onAction="#cancel"/>
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
|
@ -24,7 +24,8 @@
|
||||||
<Label text="Color" labelFor="${colorPicker}"/>
|
<Label text="Color" labelFor="${colorPicker}"/>
|
||||||
<ColorPicker fx:id="colorPicker"/>
|
<ColorPicker fx:id="colorPicker"/>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
<HBox styleClass="std-padding, std-spacing">
|
<Separator/>
|
||||||
|
<HBox styleClass="std-padding, std-spacing" alignment="CENTER_RIGHT">
|
||||||
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
<Button text="Cancel" onAction="#cancel"/>
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import com.andrewlalis.perfin.view.component.CategorySelectionBox?>
|
||||||
|
<?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.EditTransactionController"
|
fx:controller="com.andrewlalis.perfin.control.EditTransactionController"
|
||||||
|
@ -70,7 +72,7 @@
|
||||||
<Label text="Category" labelFor="${categoryComboBox}" styleClass="bold-text"/>
|
<Label text="Category" labelFor="${categoryComboBox}" styleClass="bold-text"/>
|
||||||
<Hyperlink fx:id="categoriesHyperlink" 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"/>
|
<CategorySelectionBox fx:id="categoryComboBox" maxWidth="Infinity"/>
|
||||||
|
|
||||||
<VBox>
|
<VBox>
|
||||||
<Label text="Tags" labelFor="${tagsComboBox}" styleClass="bold-text"/>
|
<Label text="Tags" labelFor="${tagsComboBox}" styleClass="bold-text"/>
|
||||||
|
@ -83,6 +85,36 @@
|
||||||
</HBox>
|
</HBox>
|
||||||
<VBox fx:id="tagsVBox" styleClass="std-spacing,std-padding"/>
|
<VBox fx:id="tagsVBox" styleClass="std-spacing,std-padding"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
|
<Label text="Line Items" styleClass="bold-text"/>
|
||||||
|
<VBox maxWidth="Infinity">
|
||||||
|
<Button text="Add Line Item" fx:id="addLineItemButton" disable="true"/>
|
||||||
|
<StyledText styleClass="small-font">
|
||||||
|
Line items aren't yet supported. I'm working on it!
|
||||||
|
</StyledText>
|
||||||
|
<VBox styleClass="std-spacing" fx:id="addLineItemForm">
|
||||||
|
<HBox styleClass="std-spacing">
|
||||||
|
<VBox>
|
||||||
|
<Label text="Quantity" styleClass="bold-text,small-font"/>
|
||||||
|
<Spinner fx:id="lineItemQuantitySpinner" minWidth="60" maxWidth="60"/>
|
||||||
|
</VBox>
|
||||||
|
<VBox HBox.hgrow="ALWAYS">
|
||||||
|
<Label text="Value per Item" styleClass="bold-text,small-font"/>
|
||||||
|
<TextField fx:id="lineItemValueField"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
<VBox>
|
||||||
|
<Label text="Description" styleClass="bold-text,small-font"/>
|
||||||
|
<TextField fx:id="lineItemDescriptionField"/>
|
||||||
|
</VBox>
|
||||||
|
<HBox styleClass="std-spacing" alignment="CENTER_RIGHT">
|
||||||
|
<Button text="Add" fx:id="addLineItemAddButton"/>
|
||||||
|
<Button text="Cancel" fx:id="addLineItemCancelButton"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
|
||||||
|
<VBox fx:id="lineItemsVBox"/>
|
||||||
|
</VBox>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
|
|
||||||
<!-- Container for attachments -->
|
<!-- Container for attachments -->
|
||||||
|
|
|
@ -24,7 +24,8 @@
|
||||||
<Label text="Description" labelFor="${descriptionField}"/>
|
<Label text="Description" labelFor="${descriptionField}"/>
|
||||||
<TextArea fx:id="descriptionField" wrapText="true"/>
|
<TextArea fx:id="descriptionField" wrapText="true"/>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
<HBox styleClass="std-padding,std-spacing">
|
<Separator/>
|
||||||
|
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_RIGHT">
|
||||||
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
<Button text="Cancel" onAction="#cancel"/>
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Food & Drink",
|
||||||
|
"color": "#10C600",
|
||||||
|
"children": [
|
||||||
|
{"name": "Groceries"},
|
||||||
|
{"name": "Restaurants"},
|
||||||
|
{"name": "Alcohol & Bars"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Transportation",
|
||||||
|
"color": "#4688FF",
|
||||||
|
"children": [
|
||||||
|
{"name": "Car & Fuel"},
|
||||||
|
{"name": "Public Transport"},
|
||||||
|
{"name": "Air Travel"},
|
||||||
|
{"name": "Taxi & Rideshare"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Household",
|
||||||
|
"color": "#E5DF00",
|
||||||
|
"children": [
|
||||||
|
{"name": "Rent & Mortgage"},
|
||||||
|
{"name": "Utilities"},
|
||||||
|
{"name": "Insurance & Fees"},
|
||||||
|
{"name": "Home Improvements & Renovation"},
|
||||||
|
{"name": "Household Supplies"},
|
||||||
|
{"name": "Pets"},
|
||||||
|
{"name": "Garden"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shopping",
|
||||||
|
"color": "#BF2484",
|
||||||
|
"children": [
|
||||||
|
{"name": "Clothes & Accessories"},
|
||||||
|
{"name": "Electronics"},
|
||||||
|
{"name": "Hobbies & Crafts"},
|
||||||
|
{"name": "Gifts"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Leisure",
|
||||||
|
"color": "#C7271C",
|
||||||
|
"children": [
|
||||||
|
{"name": "Culture & Events"},
|
||||||
|
{"name": "Movies & Media"},
|
||||||
|
{"name": "Vacation"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Health & Wellness",
|
||||||
|
"color": "#BE11D7",
|
||||||
|
"children": [
|
||||||
|
{"name": "Healthcare"},
|
||||||
|
{"name": "Medication"},
|
||||||
|
{"name": "Vision"},
|
||||||
|
{"name": "Dental"},
|
||||||
|
{"name": "Fitness & Sports"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Income",
|
||||||
|
"color": "#83F25C",
|
||||||
|
"children": [
|
||||||
|
{"name": "Cash Deposits"},
|
||||||
|
{"name": "Salary"},
|
||||||
|
{"name": "Pension"},
|
||||||
|
{"name": "Investments"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Transfers",
|
||||||
|
"color": "#F7AE39"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Charity",
|
||||||
|
"color": "#9AE5FF"
|
||||||
|
}
|
||||||
|
]
|
|
@ -173,11 +173,3 @@ CREATE TABLE account_history_item_balance_record (
|
||||||
FOREIGN KEY (record_id) REFERENCES balance_record(id)
|
FOREIGN KEY (record_id) REFERENCES balance_record(id)
|
||||||
ON UPDATE CASCADE ON DELETE CASCADE
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
/* DEFAULT ENTITIES */
|
|
||||||
INSERT INTO transaction_category (name, color) VALUES
|
|
||||||
('Food', '0dba0d'),
|
|
||||||
('Travel', '3ec0f0'),
|
|
||||||
('Utilities', '4137bf'),
|
|
||||||
('Housing', 'e6cf3c'),
|
|
||||||
('Entertainment', 'e01e1b');
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<?import javafx.scene.text.Text?>
|
<?import javafx.scene.text.Text?>
|
||||||
<?import javafx.scene.text.TextFlow?>
|
<?import javafx.scene.text.TextFlow?>
|
||||||
|
<?import javafx.scene.shape.Circle?>
|
||||||
<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.TransactionViewController"
|
fx:controller="com.andrewlalis.perfin.control.TransactionViewController"
|
||||||
|
@ -32,6 +33,37 @@
|
||||||
<Label text="Description" styleClass="bold-text"/>
|
<Label text="Description" styleClass="bold-text"/>
|
||||||
<Label fx:id="descriptionLabel" wrapText="true" style="-fx-min-height: 100px;" alignment="TOP_LEFT"/>
|
<Label fx:id="descriptionLabel" wrapText="true" style="-fx-min-height: 100px;" alignment="TOP_LEFT"/>
|
||||||
</PropertiesPane>
|
</PropertiesPane>
|
||||||
|
|
||||||
|
<PropertiesPane vgap="5" hgap="5">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints minWidth="100" halignment="LEFT" hgrow="NEVER"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
|
||||||
|
</columnConstraints>
|
||||||
|
<Label text="Vendor" styleClass="bold-text"/>
|
||||||
|
<Label fx:id="vendorLabel"/>
|
||||||
|
</PropertiesPane>
|
||||||
|
|
||||||
|
<PropertiesPane vgap="5" hgap="5">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints minWidth="100" halignment="LEFT" hgrow="NEVER"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
|
||||||
|
</columnConstraints>
|
||||||
|
<Label text="Category" styleClass="bold-text"/>
|
||||||
|
<HBox styleClass="std-spacing">
|
||||||
|
<Circle radius="8" fx:id="categoryColorIndicator"/>
|
||||||
|
<Label fx:id="categoryLabel"/>
|
||||||
|
</HBox>
|
||||||
|
</PropertiesPane>
|
||||||
|
|
||||||
|
<PropertiesPane vgap="5" hgap="5">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints minWidth="100" halignment="LEFT" hgrow="NEVER"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" halignment="LEFT"/>
|
||||||
|
</columnConstraints>
|
||||||
|
<Label text="Tags" styleClass="bold-text"/>
|
||||||
|
<Label fx:id="tagsLabel"/>
|
||||||
|
</PropertiesPane>
|
||||||
|
|
||||||
<VBox>
|
<VBox>
|
||||||
<TextFlow>
|
<TextFlow>
|
||||||
<Text text="Debited to"/>
|
<Text text="Debited to"/>
|
||||||
|
@ -45,7 +77,7 @@
|
||||||
<AttachmentsViewPane fx:id="attachmentsViewPane"/>
|
<AttachmentsViewPane fx:id="attachmentsViewPane"/>
|
||||||
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_LEFT">
|
<HBox styleClass="std-padding,std-spacing" alignment="CENTER_LEFT">
|
||||||
<Button text="Edit" onAction="#editTransaction"/>
|
<Button text="Edit" onAction="#editTransaction"/>
|
||||||
<Button text="Delete this transaction" onAction="#deleteTransaction"/>
|
<Button text="Delete" onAction="#deleteTransaction"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
</VBox>
|
</VBox>
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
|
|
Loading…
Reference in New Issue