diff --git a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java index d915c43..6150129 100644 --- a/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java +++ b/src/main/java/com/andrewlalis/perfin/control/EditTransactionController.java @@ -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() .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 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 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; diff --git a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java index 6394358..4cccda5 100644 --- a/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/TransactionsViewController.java @@ -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 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); diff --git a/src/main/java/com/andrewlalis/perfin/model/Transaction.java b/src/main/java/com/andrewlalis/perfin/model/Transaction.java index 2b451d9..8f179ca 100644 --- a/src/main/java/com/andrewlalis/perfin/model/Transaction.java +++ b/src/main/java/com/andrewlalis/perfin/model/Transaction.java @@ -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 + ); + } } diff --git a/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java b/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java deleted file mode 100644 index e4cbb21..0000000 --- a/src/main/java/com/andrewlalis/perfin/view/AccountComboBoxCellFactory.java +++ /dev/null @@ -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, ListCell> { - private final String emptyCellText; - - public AccountComboBoxCellFactory(String emptyCellText) { - this.emptyCellText = emptyCellText; - } - - public AccountComboBoxCellFactory() { - this("None"); - } - - public static class AccountListCell extends ListCell { - 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 call(ListView param) { - return new AccountListCell(emptyCellText); - } -} diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java index 6a85c23..0f0b91f 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountSelectionBox.java @@ -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 { + 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 accounts) { @@ -27,15 +29,16 @@ public class AccountSelectionBox extends ComboBox { } 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 { public final void setAllowNone(boolean value) { allowNoneProperty.set(value); } + + private static class CellFactory implements Callback, ListCell> { + @Override + public ListCell call(ListView param) { + return new AccountListCell(); + } + } + + private static class AccountListCell extends ListCell { + 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()); + } + } + } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java index ea9df27..8907f25 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/AccountTile.java @@ -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)); diff --git a/src/main/resources/transactions-view.fxml b/src/main/resources/transactions-view.fxml index ba7c4de..c94bed9 100644 --- a/src/main/resources/transactions-view.fxml +++ b/src/main/resources/transactions-view.fxml @@ -1,8 +1,8 @@ + - @@ -23,7 +23,7 @@