Added account filter to transactions list.

This commit is contained in:
Andrew Lalis 2024-01-04 19:54:13 -05:00
parent 8539ddec70
commit 9e0e0a51c5
5 changed files with 83 additions and 5 deletions

View File

@ -5,21 +5,26 @@ import com.andrewlalis.perfin.data.pagination.Page;
import com.andrewlalis.perfin.data.pagination.PageRequest; import com.andrewlalis.perfin.data.pagination.PageRequest;
import com.andrewlalis.perfin.data.pagination.Sort; import com.andrewlalis.perfin.data.pagination.Sort;
import com.andrewlalis.perfin.data.util.Pair; import com.andrewlalis.perfin.data.util.Pair;
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.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.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; 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;
import java.util.List; import java.util.List;
import java.util.Set;
import static com.andrewlalis.perfin.PerfinApp.router; import static com.andrewlalis.perfin.PerfinApp.router;
@ -35,6 +40,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 VBox transactionsVBox; @FXML public VBox transactionsVBox;
private DataSourcePaginationControls paginationControls; private DataSourcePaginationControls paginationControls;
@ -44,20 +50,40 @@ 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) -> {
paginationControls.setPage(1);
selectedTransaction.set(null);
});
this.paginationControls = new DataSourcePaginationControls( this.paginationControls = new DataSourcePaginationControls(
transactionsVBox.getChildren(), transactionsVBox.getChildren(),
new DataSourcePaginationControls.PageFetcherFunction() { new DataSourcePaginationControls.PageFetcherFunction() {
@Override @Override
public Page<? extends Node> fetchPage(PageRequest pagination) throws Exception { public Page<? extends Node> fetchPage(PageRequest pagination) throws Exception {
Account accountFilter = filterByAccountComboBox.getValue();
try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) { try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) {
return repo.findAll(pagination).map(TransactionsViewController.this::makeTile); Page<Transaction> result;
if (accountFilter == null) {
result = repo.findAll(pagination);
} else {
result = repo.findAllByAccounts(Set.of(accountFilter.id), pagination);
}
return result.map(TransactionsViewController.this::makeTile);
} }
} }
@Override @Override
public int getTotalCount() throws Exception { public int getTotalCount() throws Exception {
Account accountFilter = filterByAccountComboBox.getValue();
try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) { try (var repo = Profile.getCurrent().getDataSource().getTransactionRepository()) {
if (accountFilter == null) {
return (int) repo.countAll(); return (int) repo.countAll();
} else {
return (int) repo.countAllByAccounts(Set.of(accountFilter.id));
}
} }
} }
} }
@ -95,6 +121,21 @@ public class TransactionsViewController implements RouteSelectionListener {
paginationControls.sorts.setAll(DEFAULT_SORTS); paginationControls.sorts.setAll(DEFAULT_SORTS);
paginationControls.itemsPerPage.set(DEFAULT_ITEMS_PER_PAGE); paginationControls.itemsPerPage.set(DEFAULT_ITEMS_PER_PAGE);
// Refresh account filter options.
Thread.ofVirtual().start(() -> {
Profile.getCurrent().getDataSource().useAccountRepository(repo -> {
List<Account> accounts = repo.findAll(PageRequest.unpaged(Sort.asc("name"))).items();
accounts.add(null);
Platform.runLater(() -> {
filterByAccountComboBox.getItems().clear();
filterByAccountComboBox.getItems().addAll(accounts);
filterByAccountComboBox.getSelectionModel().selectLast();
filterByAccountComboBox.getButtonCell().updateIndex(accounts.size() - 1);
});
});
});
// If a transaction id is given in the route context, navigate to the page it's on and select it. // If a transaction id is given in the route context, navigate to the page it's on and select it.
if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) { if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) {
Thread.ofVirtual().start(() -> { Thread.ofVirtual().start(() -> {
@ -108,6 +149,7 @@ public class TransactionsViewController implements RouteSelectionListener {
}); });
} else { } else {
paginationControls.setPage(1); paginationControls.setPage(1);
selectedTransaction.set(null);
} }
} }

View File

@ -27,6 +27,7 @@ public interface TransactionRepository extends AutoCloseable {
Page<Transaction> findAll(PageRequest pagination); Page<Transaction> findAll(PageRequest pagination);
long countAll(); long countAll();
long countAllAfter(long transactionId); long countAllAfter(long transactionId);
long countAllByAccounts(Set<Long> accountIds);
Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination); Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination);
CreditAndDebitAccounts findLinkedAccounts(long transactionId); CreditAndDebitAccounts findLinkedAccounts(long transactionId);
List<Attachment> findAttachments(long transactionId); List<Attachment> findAttachments(long transactionId);

View File

@ -82,6 +82,18 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
).orElse(0L); ).orElse(0L);
} }
@Override
public long countAllByAccounts(Set<Long> accountIds) {
String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(","));
String query = String.format("""
SELECT COUNT(transaction.id)
FROM transaction
LEFT JOIN account_entry ON account_entry.transaction_id = transaction.id
WHERE account_entry.account_id IN (%s)
""", idsStr);
return DbUtil.findOne(conn, query, Collections.emptyList(), rs -> rs.getLong(1)).orElse(0L);
}
@Override @Override
public Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination) { public Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination) {
String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(",")); String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(","));

View File

@ -7,10 +7,22 @@ import javafx.scene.control.ListView;
import javafx.util.Callback; import javafx.util.Callback;
public class AccountComboBoxCellFactory implements Callback<ListView<Account>, ListCell<Account>> { 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> { public static class AccountListCell extends ListCell<Account> {
private final Label label = new Label(); private final Label label = new Label();
private final String emptyCellText;
public AccountListCell() { public AccountListCell(String emptyCellText) {
this.emptyCellText = emptyCellText;
label.setStyle("-fx-text-fill: black;"); label.setStyle("-fx-text-fill: black;");
} }
@ -18,7 +30,7 @@ public class AccountComboBoxCellFactory implements Callback<ListView<Account>, L
protected void updateItem(Account item, boolean empty) { protected void updateItem(Account item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item == null || empty) { if (item == null || empty) {
label.setText("None"); label.setText(emptyCellText);
} else { } else {
label.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")"); label.setText(item.getName() + " (" + item.getAccountNumberSuffix() + ")");
} }
@ -28,6 +40,6 @@ public class AccountComboBoxCellFactory implements Callback<ListView<Account>, L
@Override @Override
public ListCell<Account> call(ListView<Account> param) { public ListCell<Account> call(ListView<Account> param) {
return new AccountListCell(); return new AccountListCell(emptyCellText);
} }
} }

View File

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?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.ScrollPane?> <?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx" <BorderPane xmlns="http://javafx.com/javafx"
@ -15,6 +18,14 @@
<center> <center>
<HBox> <HBox>
<BorderPane fx:id="transactionsListBorderPane" HBox.hgrow="ALWAYS"> <BorderPane fx:id="transactionsListBorderPane" HBox.hgrow="ALWAYS">
<top>
<HBox styleClass="std-padding,std-spacing">
<PropertiesPane hgap="5" vgap="5">
<Label text="Filter by Account"/>
<ComboBox fx:id="filterByAccountComboBox"/>
</PropertiesPane>
</HBox>
</top>
<center> <center>
<ScrollPane fitToHeight="true" fitToWidth="true"> <ScrollPane fitToHeight="true" fitToWidth="true">
<VBox fx:id="transactionsVBox" styleClass="std-padding,spacing-extra"/> <VBox fx:id="transactionsVBox" styleClass="std-padding,spacing-extra"/>