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; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; 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.CurrencyUtil;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import com.andrewlalis.perfin.data.util.FileUtil; import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.model.CreditAndDebitAccounts; import com.andrewlalis.perfin.model.CreditAndDebitAccounts;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import com.andrewlalis.perfin.model.Transaction; import com.andrewlalis.perfin.model.Transaction;
@ -20,13 +21,14 @@ import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Currency; import java.util.Currency;
import java.util.List; import java.util.List;
@ -34,6 +36,8 @@ import java.util.List;
import static com.andrewlalis.perfin.PerfinApp.router; import static com.andrewlalis.perfin.PerfinApp.router;
public class EditTransactionController implements RouteSelectionListener { public class EditTransactionController implements RouteSelectionListener {
private static final Logger log = LoggerFactory.getLogger(EditTransactionController.class);
@FXML public Label titleLabel; @FXML public Label titleLabel;
@FXML public TextField timestampField; @FXML public TextField timestampField;
@ -67,6 +71,7 @@ public class EditTransactionController implements RouteSelectionListener {
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>() var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
.addTerminalPredicate(s -> s == null || s.length() <= 255, "Description is too long.") .addTerminalPredicate(s -> s == null || s.length() <= 255, "Description is too long.")
).validatedInitially().attach(descriptionField, descriptionField.textProperty()); ).validatedInitially().attach(descriptionField, descriptionField.textProperty());
// Linked accounts will use a property derived from both the debit and credit selections. // Linked accounts will use a property derived from both the debit and credit selections.
Property<CreditAndDebitAccounts> linkedAccountsProperty = new SimpleObjectProperty<>(getSelectedAccounts()); Property<CreditAndDebitAccounts> linkedAccountsProperty = new SimpleObjectProperty<>(getSelectedAccounts());
debitAccountSelector.valueProperty().addListener((observable, oldValue, newValue) -> linkedAccountsProperty.setValue(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); var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
saveButton.disableProperty().bind(formValid.not()); 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. // Initialize the file selection area.
attachmentsSelectionArea = new FileSelectionArea( attachmentsSelectionArea = new FileSelectionArea(
FileUtil::newAttachmentsFileChooser, FileUtil::newAttachmentsFileChooser,
@ -129,39 +129,57 @@ public class EditTransactionController implements RouteSelectionListener {
titleLabel.setText("Create New Transaction"); titleLabel.setText("Create New Transaction");
timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT)); timestampField.setText(LocalDateTime.now().format(DateUtil.DEFAULT_DATETIME_FORMAT));
amountField.setText(null); amountField.setText(null);
currencyChoiceBox.getSelectionModel().selectFirst();
descriptionField.setText(null); descriptionField.setText(null);
attachmentsSelectionArea.clear(); attachmentsSelectionArea.clear();
} else { } else {
titleLabel.setText("Edit Transaction #" + transaction.id); titleLabel.setText("Edit Transaction #" + transaction.id);
timestampField.setText(DateUtil.formatUTCAsLocal(transaction.getTimestamp())); timestampField.setText(DateUtil.formatUTCAsLocal(transaction.getTimestamp()));
amountField.setText(CurrencyUtil.formatMoneyAsBasicNumber(transaction.getMoneyAmount())); amountField.setText(CurrencyUtil.formatMoneyAsBasicNumber(transaction.getMoneyAmount()));
currencyChoiceBox.setValue(transaction.getCurrency());
descriptionField.setText(transaction.getDescription()); descriptionField.setText(transaction.getDescription());
// TODO: Add an editable list of attachments from which some can be added and removed. // 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 -> { // Fetch some account-specific data.
var currencies = repo.findAllUsedCurrencies().stream() currencyChoiceBox.setDisable(true);
.sorted(Comparator.comparing(Currency::getCurrencyCode)) creditAccountSelector.setDisable(true);
.toList(); debitAccountSelector.setDisable(true);
Platform.runLater(() -> { Thread.ofVirtual().start(() -> {
currencyChoiceBox.getItems().setAll(currencies); try (
if (creatingNew) { var accountRepo = Profile.getCurrent().getDataSource().getAccountRepository();
currencyChoiceBox.getSelectionModel().selectFirst(); var transactionRepo = Profile.getCurrent().getDataSource().getTransactionRepository()
} else { ) {
currencyChoiceBox.getSelectionModel().select(transaction.getCurrency()); 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() { private CreditAndDebitAccounts getSelectedAccounts() {
@ -190,26 +208,6 @@ public class EditTransactionController implements RouteSelectionListener {
return null; 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() { private String getSanitizedDescription() {
String raw = descriptionField.getText(); String raw = descriptionField.getText();
if (raw == null) return null; 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.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.AccountComboBoxCellFactory;
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.DataSourcePaginationControls; import com.andrewlalis.perfin.view.component.DataSourcePaginationControls;
import com.andrewlalis.perfin.view.component.TransactionTile; import com.andrewlalis.perfin.view.component.TransactionTile;
import javafx.application.Platform; import javafx.application.Platform;
@ -19,7 +19,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -44,7 +43,7 @@ public class TransactionsViewController implements RouteSelectionListener {
public record RouteContext(Long selectedTransactionId) {} public record RouteContext(Long selectedTransactionId) {}
@FXML public BorderPane transactionsListBorderPane; @FXML public BorderPane transactionsListBorderPane;
@FXML public ComboBox<Account> filterByAccountComboBox; @FXML public AccountSelectionBox filterByAccountComboBox;
@FXML public VBox transactionsVBox; @FXML public VBox transactionsVBox;
private DataSourcePaginationControls paginationControls; private DataSourcePaginationControls paginationControls;
@ -54,9 +53,6 @@ public class TransactionsViewController implements RouteSelectionListener {
@FXML public void initialize() { @FXML public void initialize() {
// Initialize the left-hand paginated transactions list. // 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) -> { filterByAccountComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
paginationControls.setPage(1); paginationControls.setPage(1);
selectedTransaction.set(null); selectedTransaction.set(null);

View File

@ -1,5 +1,7 @@
package com.andrewlalis.perfin.model; package com.andrewlalis.perfin.model;
import com.andrewlalis.perfin.data.util.DateUtil;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Currency; import java.util.Currency;
@ -43,4 +45,16 @@ public class Transaction extends IdEntity {
public MoneyValue getMoneyAmount() { public MoneyValue getMoneyAmount() {
return new MoneyValue(amount, currency); 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; package com.andrewlalis.perfin.view.component;
import com.andrewlalis.perfin.model.Account; import com.andrewlalis.perfin.model.Account;
import com.andrewlalis.perfin.view.AccountComboBoxCellFactory;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.ComboBox; 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; 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. * A box that allows the user to select one account from a list of options.
*/ */
public class AccountSelectionBox extends ComboBox<Account> { public class AccountSelectionBox extends ComboBox<Account> {
private final CellFactory cellFactory = new CellFactory();
private final BooleanProperty allowNoneProperty = new SimpleBooleanProperty(true); private final BooleanProperty allowNoneProperty = new SimpleBooleanProperty(true);
public AccountSelectionBox() { public AccountSelectionBox() {
valueProperty().bind(getSelectionModel().selectedItemProperty()); setCellFactory(cellFactory);
var factory = new AccountComboBoxCellFactory(); setButtonCell(cellFactory.call(null));
setCellFactory(factory);
setButtonCell(factory.call(null));
} }
public void setAccounts(List<Account> accounts) { public void setAccounts(List<Account> accounts) {
@ -27,15 +29,16 @@ public class AccountSelectionBox extends ComboBox<Account> {
} }
getItems().clear(); getItems().clear();
getItems().addAll(accounts); getItems().addAll(accounts);
int idx;
if (getAllowNone()) { if (getAllowNone()) {
getSelectionModel().select(null); getSelectionModel().select(null);
idx = accounts.indexOf(null);
} else { } else {
getSelectionModel().clearSelection(); getSelectionModel().clearSelection();
idx = 0;
} }
getButtonCell().updateIndex(idx); }
public void select(Account account) {
setButtonCell(cellFactory.call(null));
getSelectionModel().select(account);
} }
public final BooleanProperty allowNoneProperty() { public final BooleanProperty allowNoneProperty() {
@ -49,4 +52,30 @@ public class AccountSelectionBox extends ComboBox<Account> {
public final void setAllowNone(boolean value) { public final void setAllowNone(boolean value) {
allowNoneProperty.set(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) { public AccountTile(Account account) {
setMinWidth(300.0);
setPrefWidth(350.0); setPrefWidth(350.0);
setMaxWidth(400.0);
getStyleClass().addAll("tile", "hand-cursor"); getStyleClass().addAll("tile", "hand-cursor");
setTop(getHeader(account)); setTop(getHeader(account));

View File

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