Added AccountSelectionBox and cleaned up logic in the EditTransactionController quite a bit.

This commit is contained in:
Andrew Lalis 2024-01-12 11:30:59 -05:00
parent 3521dee149
commit 1a40b78a70
7 changed files with 106 additions and 113 deletions

View File

@ -1,10 +1,11 @@
package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.pagination.Sort;
import com.andrewlalis.perfin.data.util.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction;
@ -20,13 +21,14 @@ import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Currency;
import java.util.List;
@ -34,6 +36,8 @@ import java.util.List;
import static com.andrewlalis.perfin.PerfinApp.router;
public class EditTransactionController implements RouteSelectionListener {
private static final Logger log = LoggerFactory.getLogger(EditTransactionController.class);
@FXML public Label titleLabel;
@FXML public TextField timestampField;
@ -67,6 +71,7 @@ public class EditTransactionController implements RouteSelectionListener {
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
.addTerminalPredicate(s -> s == null || s.length() <= 255, "Description is too long.")
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
// Linked accounts will use a property derived from both the debit and credit selections.
Property<CreditAndDebitAccounts> linkedAccountsProperty = new SimpleObjectProperty<>(getSelectedAccounts());
debitAccountSelector.valueProperty().addListener((observable, oldValue, newValue) -> linkedAccountsProperty.setValue(getSelectedAccounts()));
@ -82,11 +87,6 @@ public class EditTransactionController implements RouteSelectionListener {
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
saveButton.disableProperty().bind(formValid.not());
// Update the lists of accounts available for linking based on the selected currency.
currencyChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> {
updateLinkAccountComboBoxes(newValue);
});
// Initialize the file selection area.
attachmentsSelectionArea = new FileSelectionArea(
FileUtil::newAttachmentsFileChooser,
@ -129,39 +129,57 @@ public class EditTransactionController implements RouteSelectionListener {
titleLabel.setText("Create New Transaction");
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
amountField.setText(null);
currencyChoiceBox.getSelectionModel().selectFirst();
descriptionField.setText(null);
attachmentsSelectionArea.clear();
} else {
titleLabel.setText("Edit Transaction #" + transaction.id);
timestampField.setText(DateUtil.formatUTCAsLocal(transaction.getTimestamp()));
amountField.setText(CurrencyUtil.formatMoneyAsBasicNumber(transaction.getMoneyAmount()));
currencyChoiceBox.setValue(transaction.getCurrency());
descriptionField.setText(transaction.getDescription());
// TODO: Add an editable list of attachments from which some can be added and removed.
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.id);
Platform.runLater(() -> {
debitAccountSelector.getSelectionModel().select(accounts.debitAccount());
creditAccountSelector.getSelectionModel().select(accounts.creditAccount());
});
}));
}
Thread.ofVirtual().start(() -> Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
var currencies = repo.findAllUsedCurrencies().stream()
.sorted(Comparator.comparing(Currency::getCurrencyCode))
.toList();
Platform.runLater(() -> {
currencyChoiceBox.getItems().setAll(currencies);
if (creatingNew) {
currencyChoiceBox.getSelectionModel().selectFirst();
} else {
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
}
});
}));
// Fetch some account-specific data.
currencyChoiceBox.setDisable(true);
creditAccountSelector.setDisable(true);
debitAccountSelector.setDisable(true);
Thread.ofVirtual().start(() -> {
try (
var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
var transactionRepo = Profile.getCurrent().getDataSource().getTransactionRepository()
) {
var currencies = accountRepo.findAllUsedCurrencies().stream()
.sorted(Comparator.comparing(Currency::getCurrencyCode))
.toList();
var accounts = accountRepo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
CreditAndDebitAccounts linkedAccounts = transaction == null ? null : transactionRepo.findLinkedAccounts(transaction.id);
Platform.runLater(() -> {
creditAccountSelector.setAccounts(accounts);
debitAccountSelector.setAccounts(accounts);
currencyChoiceBox.getItems().setAll(currencies);
if (creatingNew) {
// TODO: Allow user to select a default currency.
currencyChoiceBox.getSelectionModel().selectFirst();
creditAccountSelector.select(null);
debitAccountSelector.select(null);
} else {
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency());
if (linkedAccounts != null) {
creditAccountSelector.select(linkedAccounts.creditAccount());
debitAccountSelector.select(linkedAccounts.debitAccount());
}
}
currencyChoiceBox.setDisable(false);
creditAccountSelector.setDisable(false);
debitAccountSelector.setDisable(false);
});
} catch (Exception e) {
log.error("Failed to get repositories.", e);
Popups.error("Failed to fetch account-specific data: " + e.getMessage());
}
});
}
private CreditAndDebitAccounts getSelectedAccounts() {
@ -190,26 +208,6 @@ public class EditTransactionController implements RouteSelectionListener {
return null;
}
private void updateLinkAccountComboBoxes(Currency currency) {
Thread.ofVirtual().start(() -> {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
List<Account> availableAccounts = new ArrayList<>();
if (currency != null) availableAccounts.addAll(repo.findAllByCurrency(currency));
Platform.runLater(() -> {
debitAccountSelector.setAccounts(availableAccounts);
creditAccountSelector.setAccounts(availableAccounts);
if (transaction != null) {
Profile.getCurrent().getDataSource().useTransactionRepository(transactionRepo -> {
var linkedAccounts = transactionRepo.findLinkedAccounts(transaction.id);
debitAccountSelector.getSelectionModel().select(linkedAccounts.debitAccount());
creditAccountSelector.getSelectionModel().select(linkedAccounts.creditAccount());
});
}
});
});
});
}
private String getSanitizedDescription() {
String raw = descriptionField.getText();
if (raw == null) return null;

View File

@ -9,8 +9,8 @@ import com.andrewlalis.perfin.data.util.Pair;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction;
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
import com.andrewlalis.perfin.view.SceneUtil;
import com.andrewlalis.perfin.view.component.AccountSelectionBox;
import com.andrewlalis.perfin.view.component.DataSourcePaginationControls;
import com.andrewlalis.perfin.view.component.TransactionTile;
import javafx.application.Platform;
@ -19,7 +19,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
@ -44,7 +43,7 @@ public class TransactionsViewController implements RouteSelectionListener {
public record RouteContext(Long selectedTransactionId) {}
@FXML public BorderPane transactionsListBorderPane;
@FXML public ComboBox<Account> filterByAccountComboBox;
@FXML public AccountSelectionBox filterByAccountComboBox;
@FXML public VBox transactionsVBox;
private DataSourcePaginationControls paginationControls;
@ -54,9 +53,6 @@ public class TransactionsViewController implements RouteSelectionListener {
@FXML public void initialize() {
// Initialize the left-hand paginated transactions list.
var accountCellFactory = new AccountComboBoxCellFactory("All");
filterByAccountComboBox.setCellFactory(accountCellFactory);
filterByAccountComboBox.setButtonCell(accountCellFactory.call(null));
filterByAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
paginationControls.setPage(1);
selectedTransaction.set(null);

View File

@ -1,5 +1,7 @@
package com.andrewlalis.perfin.model;
import com.andrewlalis.perfin.data.util.DateUtil;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Currency;
@ -43,4 +45,16 @@ public class Transaction extends IdEntity {
public MoneyValue getMoneyAmount() {
return new MoneyValue(amount, currency);
}
@Override
public String toString() {
return String.format(
"Transaction (id=%d, timestamp=%s, amount=%s, currency=%s, description=%s)",
id,
timestamp.format(DateUtil.DEFAULT_DATETIME_FORMAT),
amount.toPlainString(),
currency.getCurrencyCode(),
description
);
}
}

View File

@ -1,46 +0,0 @@
package com.andrewlalis.perfin.view;
import com.andrewlalis.perfin.model.Account;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;
public class AccountComboBoxCellFactory implements Callback<ListView<Account>, ListCell<Account>> {
private final String emptyCellText;
public AccountComboBoxCellFactory(String emptyCellText) {
this.emptyCellText = emptyCellText;
}
public AccountComboBoxCellFactory() {
this("None");
}
public static class AccountListCell extends ListCell<Account> {
private final Label label = new Label();
private final String emptyCellText;
public AccountListCell(String emptyCellText) {
this.emptyCellText = emptyCellText;
label.getStyleClass().add("normal-color-text-fill");
setGraphic(label);
}
@Override
protected void updateItem(Account item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
label.setText(emptyCellText);
} else {
label.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")");
}
setGraphic(label);
}
}
@Override
public ListCell<Account> call(ListView<Account> param) {
return new AccountListCell(emptyCellText);
}
}

View File

@ -1,10 +1,13 @@
package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;
import java.util.List;
@ -12,13 +15,12 @@ import java.util.List;
* A box that allows the user to select one account from a list of options.
*/
public class AccountSelectionBox extends ComboBox<Account> {
private final CellFactory cellFactory = new CellFactory();
private final BooleanProperty allowNoneProperty = new SimpleBooleanProperty(true);
public AccountSelectionBox() {
valueProperty().bind(getSelectionModel().selectedItemProperty());
var factory = new AccountComboBoxCellFactory();
setCellFactory(factory);
setButtonCell(factory.call(null));
setCellFactory(cellFactory);
setButtonCell(cellFactory.call(null));
}
public void setAccounts(List<Account> accounts) {
@ -27,15 +29,16 @@ public class AccountSelectionBox extends ComboBox<Account> {
}
getItems().clear();
getItems().addAll(accounts);
int idx;
if (getAllowNone()) {
getSelectionModel().select(null);
idx = accounts.indexOf(null);
} else {
getSelectionModel().clearSelection();
idx = 0;
}
getButtonCell().updateIndex(idx);
}
public void select(Account account) {
setButtonCell(cellFactory.call(null));
getSelectionModel().select(account);
}
public final BooleanProperty allowNoneProperty() {
@ -49,4 +52,30 @@ public class AccountSelectionBox extends ComboBox<Account> {
public final void setAllowNone(boolean value) {
allowNoneProperty.set(value);
}
private static class CellFactory implements Callback<ListView<Account>, ListCell<Account>> {
@Override
public ListCell<Account> call(ListView<Account> param) {
return new AccountListCell();
}
}
private static class AccountListCell extends ListCell<Account> {
private final Label label = new Label();
public AccountListCell() {
setGraphic(label);
label.getStyleClass().add("normal-color-text-fill");
}
@Override
protected void updateItem(Account item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
label.setText("None");
} else {
label.setText(item.getName() + " " + item.getAccountNumberSuffix());
}
}
}
}

View File

@ -31,7 +31,9 @@ public class AccountTile extends BorderPane {
);
public AccountTile(Account account) {
setMinWidth(300.0);
setPrefWidth(350.0);
setMaxWidth(400.0);
getStyleClass().addAll("tile", "hand-cursor");
setTop(getHeader(account));

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.andrewlalis.perfin.view.component.AccountSelectionBox?>
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?>
@ -23,7 +23,7 @@
<HBox styleClass="std-padding,std-spacing">
<PropertiesPane hgap="5" vgap="5">
<Label text="Filter by Account"/>
<ComboBox fx:id="filterByAccountComboBox"/>
<AccountSelectionBox fx:id="filterByAccountComboBox"/>
</PropertiesPane>
</HBox>
</top>