Added AccountSelectionBox and cleaned up logic in the EditTransactionController quite a bit.
This commit is contained in:
parent
3521dee149
commit
1a40b78a70
|
@ -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()
|
||||
// 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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue